├── .coveralls.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── docs ├── configuration.md ├── customisation.md ├── examples.md ├── examples │ ├── UploadAndDeleteImageListener.md │ └── UploadFilenameListener.md ├── faq.md ├── installation.md ├── shell.md ├── upgrading.md └── validation.md ├── phpstan.neon ├── phpunit.xml ├── src ├── Database │ └── Type │ │ └── FileType.php ├── Exception │ ├── CannotUploadFileException.php │ └── InvalidClassException.php ├── Lib │ ├── ImageTransform.php │ ├── ImageTransformInterface.php │ ├── ProfferPath.php │ └── ProfferPathInterface.php ├── Model │ ├── Behavior │ │ └── ProfferBehavior.php │ └── Validation │ │ └── ProfferRules.php ├── Plugin.php └── Shell │ └── ProfferShell.php └── tests ├── Fixture ├── image_480x640.jpg └── image_640x480.jpg ├── Stubs ├── BadPath.php ├── TestPath.php └── TestTransform.php ├── TestCase ├── Lib │ └── ProfferPathTest.php └── Model │ ├── Behavior │ └── ProfferBehaviorTest.php │ └── Validation │ └── ProfferRulesTest.php └── bootstrap.php /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-ci 2 | coverage_clover: build/logs/clover.xml 3 | json_path: build/logs/coveralls-upload.json 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tmp/* 2 | [Cc]onfig/core.php 3 | [Cc]onfig/database.php 4 | app/tmp/* 5 | app/[Cc]onfig/core.php 6 | app/[Cc]onfig/database.php 7 | !empty 8 | 9 | /vendor/ 10 | composer.lock 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | #This Travis config template file was taken from https://github.com/FriendsOfCake/travis 2 | language: php 3 | 4 | php: 5 | - 5.6 6 | - 7.0 7 | 8 | sudo: false 9 | 10 | env: 11 | global: 12 | - DEFAULT=1 13 | 14 | matrix: 15 | fast_finish: true 16 | 17 | include: 18 | php: 5.6 19 | env: PHPCS=1 DEFAULT=0 20 | 21 | php: 5.6 22 | env: COVERALLS=1 DEFAULT=0 23 | 24 | install: 25 | - composer self-update 26 | - composer install --prefer-dist --no-interaction --dev 27 | 28 | before_script: 29 | - sh -c "if [ '$PHPCS' = '1' ]; then composer require cakephp/cakephp-codesniffer:~2; fi" 30 | - sh -c "if [ '$COVERALLS' = '1' ]; then composer require --dev satooshi/php-coveralls:~1.0; fi" 31 | - sh -c "if [ '$COVERALLS' = '1' ]; then mkdir -p src/build/logs; fi" 32 | 33 | - phpenv rehash 34 | - set +H 35 | 36 | script: 37 | - sh -c "if [ '$DEFAULT' = '1' ]; then vendor/bin/phpunit --stderr; fi" 38 | - sh -c "if [ '$PHPCS' = '1' ]; then vendor/bin/phpcs -p --ignore=bootstrap.php --standard=vendor/cakephp/cakephp-codesniffer/CakePHP ./src ./tests; fi" 39 | - sh -c "if [ '$COVERALLS' = '1' ]; then vendor/bin/phpunit --stderr --coverage-clover src/build/logs/clover.xml; fi" 40 | - sh -c "if [ '$COVERALLS' = '1' ]; then php vendor/bin/coveralls -c .coveralls.yml -v --root_dir ./src; fi" 41 | 42 | after_script: 43 | - sh -c "cat build/logs/clover.xml" 44 | 45 | notifications: 46 | email: false 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 David Yell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # :fallen_leaf: Archived 2 | I'm very sorry, but I have not worked on this project for a long time and so I have chosen to archive it. The CakePHP 4 version of the plugin never got to a release stage. 3 | 4 | I would recommend checking out https://github.com/FriendsOfCake/cakephp-upload which provides similar functionality, except the image manipulation, and is activly maintained. 5 | 6 | Thanks to everyone who contributed! 7 | 8 | 9 | ---- 10 | 11 | 12 | # CakePHP3-Proffer 13 | An upload plugin for CakePHP 3. Looking for CakePHP 4? Check out the `cake-4` branch. 14 | 15 | ![Proffer definition](http://i.imgur.com/OaAqQ6x.png) 16 | 17 | ## What is it? 18 | So I needed a way to upload images in [CakePHP 3](http://github.com/cakephp/cakephp), and as I couldn't find anything 19 | that I liked I decided to write my own in a similar vein to how [@josegonzalez](https://github.com/josegonzalez) had 20 | written his [CakePHP-Upload](https://github.com/josegonzalez/cakephp-upload) plugin for CakePHP 2. 21 | 22 | ## Requirements 23 | * PHP 5.6+ 24 | * Database 25 | * CakePHP 3 26 | * [Composer](http://getcomposer.org/) 27 | * [File Info is enabled](http://php.net/manual/en/book.fileinfo.php) for mimetype validation 28 | 29 | For more requirements, please check the `composer.json` file in the repository. 30 | 31 | This plugin implements the [Intervention](http://image.intervention.io/) image library. 32 | 33 | ## Status 34 | [![Build Status](https://travis-ci.org/davidyell/CakePHP3-Proffer.svg?branch=master)](https://travis-ci.org/davidyell/CakePHP3-Proffer) 35 | [![Coverage Status](https://coveralls.io/repos/davidyell/CakePHP3-Proffer/badge.png)](https://coveralls.io/r/davidyell/CakePHP3-Proffer) 36 | [![Dependency Status](https://www.versioneye.com/user/projects/54eee43931e55e12f9000018/badge.svg?style=flat)](https://www.versioneye.com/user/projects/54eee43931e55e12f9000018) 37 | [![Latest Stable Version](https://poser.pugx.org/davidyell/proffer/v/stable.svg)](https://packagist.org/packages/davidyell/proffer) [![Total Downloads](https://poser.pugx.org/davidyell/proffer/downloads.svg)](https://packagist.org/packages/davidyell/proffer) [![Latest Unstable Version](https://poser.pugx.org/davidyell/proffer/v/unstable.svg)](https://packagist.org/packages/davidyell/proffer) [![License](https://poser.pugx.org/davidyell/proffer/license.svg)](https://packagist.org/packages/davidyell/proffer) 38 | [![SensioLabsInsight](https://insight.sensiolabs.com/projects/65daa950-3128-44ef-b388-d4370efd853c/mini.png)](https://insight.sensiolabs.com/projects/65daa950-3128-44ef-b388-d4370efd853c) 39 | 40 | ## Documentation 41 | All the documentation can be found in the [docs](docs) folder. 42 | * [Installation](docs/installation.md) 43 | * [Configuration](docs/configuration.md) 44 | * [Validation](docs/validation.md) 45 | * [Customisation](docs/customisation.md) 46 | * [Shell tasks](docs/shell.md) 47 | * [Examples](docs/examples.md) 48 | * [FAQ](docs/faq.md) 49 | * [Upgrading](docs/upgrading.md) 50 | 51 | ## Contribution 52 | Please open a pull request or submit an issue if there is anything you would like to contribute. Please write a test for 53 | any new functionality that you add and be sure to run the tests before you commit. Also don't forget to run PHPCS with 54 | the PSR2 standard to avoid errors in TravisCI. 55 | 56 | :warning: Please target all new PRs at the `develop` branch. 57 | 58 | ## License 59 | Please see [LICENSE](LICENSE) 60 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "davidyell/proffer", 3 | "description": "An upload plugin for CakePHP 3", 4 | "type": "cakephp-plugin", 5 | "keywords": ["cakephp", "cakephp3", "upload", "file", "image", "orm"], 6 | "homepage": "https://github.com/davidyell/CakePHP3-Proffer", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "David Yell", 11 | "email": "neon1024@gmail.com" 12 | } 13 | ], 14 | "support": { 15 | "irc": "irc://irc.freenode.org/cakephp", 16 | "issues": "https://github.com/davidyell/CakePHP3-Proffer/issues", 17 | "source": "https://github.com/davidyell/CakePHP3-Proffer" 18 | }, 19 | "require": { 20 | "php": ">=5.6.0", 21 | "cakephp/orm": "3.*", 22 | "intervention/image": "^2.3" 23 | }, 24 | "require-dev": { 25 | "phpunit/phpunit": "^5|^6", 26 | "cakephp/cakephp": "~3.4", 27 | "cakephp/cakephp-codesniffer": "~3.0" 28 | }, 29 | "autoload": { 30 | "psr-4": { 31 | "Proffer\\": "src" 32 | } 33 | }, 34 | "autoload-dev": { 35 | "psr-4": { 36 | "Proffer\\Tests\\": "tests/TestCase", 37 | "Proffer\\Tests\\Fixture\\": "tests/Fixture", 38 | "Proffer\\Tests\\Stubs\\": "tests/Stubs" 39 | } 40 | }, 41 | "scripts": { 42 | "cs-check": "phpcs --colors -p --standard=vendor/cakephp/cakephp-codesniffer/CakePHP src/ tests/", 43 | "cs-fix": "phpcbf --colors -p --standard=vendor/cakephp/cakephp-codesniffer/CakePHP src/ tests/", 44 | "test": "phpunit --colors=always" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | This manual page relates to how to configure the Proffer behaviour, what the configuration options do, their defaults and how to change them. 3 | 4 | ## Configuring the behaviour in your table 5 | You will need to add a few things to your Table class. 6 | 7 | Below is an example setup, which also includes some of the defaults so you can see what they look like. You can check the options below to 8 | see which ones you must define and which ones can be ignored to use the defaults. 9 | 10 | ```php 11 | addBehavior('Proffer.Proffer', [ 14 | 'photo' => [ // The name of your upload field 15 | 'root' => WWW_ROOT . 'files', // Customise the root upload folder here, or omit to use the default 16 | 'dir' => 'photo_dir', // The name of the field to store the folder 17 | 'thumbnailSizes' => [ // Declare your thumbnails 18 | 'square' => [ // Define the prefix of your thumbnail 19 | 'w' => 200, // Width 20 | 'h' => 200, // Height 21 | 'jpeg_quality' => 100 22 | ], 23 | 'portrait' => [ // Define a second thumbnail 24 | 'w' => 100, 25 | 'h' => 300 26 | ], 27 | ], 28 | 'thumbnailMethod' => 'gd' // Options are Imagick or Gd 29 | ] 30 | ]); 31 | ``` 32 | 33 | Each upload field should have an array of settings which control the options for that upload field. In the example 34 | above my upload field is called `photo` and I pass an array of options, namely the name of the field to store the 35 | directory in. 36 | 37 | * By default generated thumbnail images will be set to the highest image quality in the `ImageTransform` class. 38 | * By default files will be uploaded to `/webroot/files///`. 39 | 40 | ### Thumbnail methods 41 | Additional thumbnail generation types are available using the `crop` and `fit` options, in the thumbnail configuration. 42 | 43 | ```php 44 | 'square' => [ 45 | 'w' => 200, 46 | 'h' => 200, 47 | 'fit' => true 48 | ], 49 | 'portrait' => [ 50 | 'w' => 150, 51 | 'h' => 300, 52 | 'crop' => true, 53 | 'orientate' => true 54 | ] 55 | ``` 56 | 57 | #### Fit 58 | > Combine cropping and resizing to format image in a smart way. The method will find the best fitting aspect ratio of 59 | > your given width and height on the current image automatically, cut it out and resize it to the given dimension. 60 | See [Intervention Fit method](http://image.intervention.io/api/fit) 61 | 62 | #### Crop 63 | > Cut out a rectangular part of the current image with given width and height. 64 | By default, will be the centre of the image. 65 | See [Intervention Crop method](http://image.intervention.io/api/crop) 66 | 67 | #### Orientate 68 | > Reads the EXIF image profile setting 'Orientation' and performs a rotation on the image to display the image correctly. 69 | See [Intervention Orientate method](http://image.intervention.io/api/orientate) for PHP installation requirements. 70 | 71 | ## Template 72 | In order to upload a file to your application you will need to add the form fields to your view. 73 | ```php 74 | echo $this->Form->create($entity, ['type' => 'file']); // Dont miss this out or no files will upload 75 | echo $this->Form->input('photo', ['type' => 'file']); 76 | echo $this->Form->button(__('Submit')); 77 | echo $this->Form->end(); 78 | ``` 79 | This will turn your form into a multipart form and add the relevant fields. 80 | 81 | ## Configuration options 82 | There are a number of configuration options you can pass into the behaviour when you attach it to your table. These options are passed as an array value of the upload field. 83 | 84 | ### dir 85 | **required** `string` 86 | The database field which will store the name of the folder in which the files are uploaded. 87 | 88 | ### thumbnailSizes 89 | **optional** `array` 90 | 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. 91 | Eg, `'square' => ['w' => 200, 'h' => 200]` would create a thumbnail prefixed with `square_` and would be 100px x 100px. 92 | If you do not specify the `thumbnailSizes` configuration option, no thumbnails will be created. 93 | 94 | ### root 95 | **optional:** defaults to, `WWW_DIR . 'files'` 96 | Allows you to customise the root folder in which all the file upload folders and files will be created. 97 | 98 | ### thumbnailMethod 99 | **optional:** defaults to, `gd` 100 | Which Intervention engine to use to convert the images. Defaults to PHP's GD library. Can also be `imagick`. 101 | 102 | ### pathClass 103 | **optional** 104 | If you want to inject your own class for dealing with paths you can specify it here as a fully qualified namespace. 105 | Eg, `'pathClass' => App\Lib\Proffer\AvatarPath::class` 106 | 107 | ### transformClass 108 | **optional** 109 | If you want to replace the creation of thumbnails you can specify your own class here, it must be a fully qualified namespace. 110 | EG, `'transformClass' => App\Lib\Proffer\WatermarkThumbnail::class`. 111 | 112 | ## Associating many uploads to a parent 113 | If you need to associate many uploads to a single parent entity, the same process as above applies, but you should attach 114 | and configure the behaviour on the association. 115 | 116 | Let's look at an example. 117 | 118 | ```php 119 | // Posts hasMany Uploads 120 | // ! Remember to add a `post_id` field to your associated `uploads` database table. 121 | 122 | // App\Model\Table\PostsTable::initialize 123 | $this->hasMany('Uploads'); 124 | 125 | // App\Model\Table\UploadsTable::initialize 126 | $this->addBehavior('Proffer.Proffer', [ 127 | 'photo' => [ 128 | 'dir' => 'photo_dir' 129 | ] 130 | ]); 131 | ``` 132 | 133 | Now, when you save a post, with associated Uploads data, each upload will be converted to an entity, and saved. 134 | 135 | ### Uploading multiple files 136 | So now you've configured the behaviour and created the table associations, you'll need to get the request data. If you're 137 | using HTML5, then you can use the file input, with the `multiple` flag, to allow for multiple file upload fields. Older 138 | browsers will see this as a single file upload field instead of multiple. 139 | 140 | :warning: Note that the field name is an array! 141 | 142 | ```php 143 | // Template/Posts/add.ctp 144 | echo $this->Form->input('filename[]', ['type' => 'file', 'multiple' => true, 'label' => 'Files to upload']); 145 | ``` 146 | 147 | ## Configuring your templates 148 | You will need to make sure that your forms are using the file type so that the files can be uploaded. 149 | 150 | ```php 151 | echo $this->Form->create($entity, ['type' => 'file']); 152 | echo $this->Form->input('photo', ['type' => 'file']); 153 | // etc 154 | ``` 155 | 156 | [< Installation](installation.md) | [Validation >](validation.md) 157 | -------------------------------------------------------------------------------- /docs/customisation.md: -------------------------------------------------------------------------------- 1 | # Customisation 2 | This manual page deals with customising the behaviour of the Proffer plugin. How to change the upload location and changing 3 | file names. It also cover how you can use the Proffer events to change the way the plugin behaves. 4 | 5 | ## Customising using an event listener 6 | 7 | ### Customising upload file names and paths 8 | Using the `Proffer.afterPath` event you can hook into all the details about the file upload before it is processed. Using 9 | this event you can change the name of the file and the upload path to match whatever convention you want. I have created 10 | an example listener which is [available as an example](examples/UploadFilenameListener.md). 11 | 12 | ```php 13 | // In your Table classes 'initialize()' method 14 | 15 | // Add your new custom listener 16 | $listener = new App\Event\LogFilenameListener(); 17 | $this->eventManager()->on($listener); 18 | ``` 19 | 20 | The advantages of customisation using a listener is that you can encapsulate the file naming functionality into a single 21 | file and also attach this listener to multiple tables, if you wanted the same naming convention in multiple places. 22 | 23 | [You can read more about Event Listeners in the book.](http://book.cakephp.org/3.0/en/core-libraries/events.html) 24 | 25 | :warning: The listener will overwrite any settings that are configured in the path class. This includes if you are using 26 | your own path class. 27 | 28 | ### Customising behavior of file creation/deletion 29 | Proffer’s image creation can be hooked by using `Proffer.afterCreateImage` event, and by using `Proffer.beforeDeleteImage` event, Proffer’s image deletion can be hooked. 30 | These events can be used to copy files to external services (e.g. Amazon S3), or deleting files from external services at the same time of Proffer creating/deleting images. 31 | I have created an example listener which is [available as an example](examples/UploadAndDeleteImageListener.md). 32 | 33 | ## Advanced customisation 34 | If you want more control over how the plugin is handling paths or creating thumbnails you can replace these components 35 | with your own by creating a class using the provided interfaces and injecting them into the plugin. 36 | 37 | In your table classes configuration you can add the `pathClass` and `transformClass` configuration options and specify a 38 | fully namespaced class name as a string. When the plugin runs it will look for and instantiate these classes. 39 | 40 | An example might look like this. 41 | 42 | ```php 43 | // src/Model/Table/ExamplesTable.php 44 | $this->addBehavior('Proffer.Proffer', [ 45 | 'image' => [ 46 | 'dir' => 'image_dir', 47 | 'thumbnailSizes' => [ 48 | 'square' => ['w' => 100, 'h' => 100] 49 | ], 50 | 'pathClass' => '\App\Lib\Proffer\UserProfilePath', 51 | 'transformClass' => '\App\Lib\Proffer\UserProfileAvatar' 52 | ] 53 | ]); 54 | ``` 55 | 56 | The configuration options are covered in the [configuration documentation](configuration.md). 57 | 58 | ### Using the interfaces 59 | Using the configuration above, you can completely change the implementation of these core classes if you want to, by 60 | creating your own. Make sure that they implement the correct interface so that the plugin will still work. 61 | 62 | ```php 63 | // src/Lib/Proffer/UserProfilePath.php 64 | class UserProfilePath implements Proffer\Lib\ProfferPathInterface 65 | { 66 | // Create the stub methods and implement your code here 67 | } 68 | 69 | // src/Lib/Proffer/UserProfileAvatar.php 70 | class UserProfileAvatar implements Proffer\Lib\ImageTransformInterface 71 | { 72 | // Create the stub methods and implement your code here 73 | } 74 | ``` 75 | 76 | ### Extending the plugin classes 77 | Using the configuration above you can also customise specific methods by extending the plugin classes and overriding 78 | their methods. 79 | 80 | ```php 81 | // src/Lib/Proffer/UserProfilePath.php 82 | class UserProfilePath extends Proffer\Lib\ProfferPath 83 | { 84 | public function generateSeed($seed) 85 | { 86 | if ($seed) { 87 | return $seed; 88 | } 89 | 90 | return date('Y-m-d-H-i-s'); 91 | } 92 | } 93 | ``` 94 | 95 | [< Validation](validation.md) | [Shell tasks >](shell.md) 96 | -------------------------------------------------------------------------------- /docs/examples.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | This manual page shows some examples of how to customise the behaviour of the plugin, 3 | as well as event listeners and image display. 4 | 5 | ## Displaying uploaded images 6 | You can use the `HtmlHelper` to link the images. Just make sure that you have both upload fields in the data set to the view. 7 | This is what it would look like if you're using the defaults, if you've implemented your own path class, you will need 8 | to update the paths accordingly. 9 | ```php 10 | echo $this->Html->image('../files/
//' . $data->get('image_dir') . '/_' . $data->get('image')); 11 | ``` 12 | 13 | ## Example event listener 14 | Here are some basic event listener example classes 15 | * [Customize the upload folder and filename](examples/UploadFilenameListener.md) 16 | * [Customize behavior of file creation/deletion](examples/UploadAndDeleteImageListener.md) 17 | 18 | ## Uploading multiple related images 19 | This example will show you how to upload many images which are related to your 20 | current table class. An example setup might be that you have a `Users` table class 21 | and a `UserImages` table class. The example below is just [baked code](http://book.cakephp.org/3.0/en/bake/usage.html). 22 | 23 | ### Tables 24 | The relationships are setup as follows. Be sure to attach the behavior to the 25 | table class which is receiving the uploads. 26 | 27 | ```php 28 | // src/Model/Table/UsersTable.php 29 | $this->hasMany('UserImages', ['foreignKey' => 'user_id']); 30 | 31 | // src/Model/Table/UserImagesTable.php 32 | $this->addBehavior('Proffer.Proffer', [ 33 | 'image' => [ 34 | 'dir' => 'image_dir', 35 | 'thumbnailSizes' => [ 36 | 'square' => ['w' => 100, 'h' => 100], 37 | 'large' => ['w' => 250, 'h' => 250] 38 | ] 39 | ] 40 | ]); 41 | 42 | $this->belongsTo('Users', ['foreignKey' => 'user_id', 'joinType' => 'INNER']); 43 | ``` 44 | 45 | ### Entities 46 | Your entity must allow the associated field in it's `$_accessible` array. So in our 47 | example we need to check that the `'user_images' => true` is included in our `User` entity. 48 | 49 | ### Controller 50 | No changes need to be made to standard controller code as Cake will automatically save any 51 | first level associated data by default. As our `Users` table is directly associated with 52 | our `UserImages` table, we don't need to change anything. 53 | 54 | If you were working with a related models data, you would need to specify the associations 55 | to populate when [merging the entity data](http://book.cakephp.org/3.0/en/orm/saving-data.html#converting-request-data-into-entities) 56 | using the `'associated'` key. 57 | 58 | ### Templates 59 | You will need to include the related fields in your templates using the correct 60 | field names, so that your request data is formatted correctly. 61 | 62 | ```php 63 | // Don't forget that you need to include ['type' => 'file'] in your ->create() call 64 |
65 | User images 66 | Form->input('user_images.0.image', ['type' => 'file']); 68 | echo $this->Form->input('user_images.1.image', ['type' => 'file']); 69 | echo $this->Form->input('user_images.2.image', ['type' => 'file']); 70 | ?> 71 |
72 | ``` 73 | 74 | How you deal with the display of existing images, deletion of existing images, 75 | and adding of new upload fields is up to you, and outside the scope of this example. 76 | 77 | ### Deleting images but preserving data 78 | If you need to delete an upload and remove it's associated data from your data store, you can achieve this in your controller. 79 | 80 | The easiest way is to add a checkbox to your form and then look for it when processing your post data. 81 | 82 | An example form might look like. It's important to note that I've disabled the `hiddenField` option here. 83 | 84 | ```php 85 | echo $this->Form->input('cover', ['type' => 'file']); 86 | if (!empty($league->cover)) { 87 | echo $this->Form->input('delete_cover', ['type' => 'checkbox', 'hiddenField' => false, 'label' => 'Remove my cover photo']); 88 | } 89 | ``` 90 | 91 | Then in your controller, check for the field before using `patchEntity` 92 | 93 | ```php 94 | // Deleting the upload? 95 | if (isset($this->request->data['delete_cover'])) { 96 | $this->request->data['image_dir'] = null; 97 | $this->request->data['cover'] = null; 98 | 99 | $path = new \Proffer\Lib\ProfferPath($this->Leagues, $league, 'cover', $this->Leagues->behaviors()->Proffer->config('cover')); 100 | $path->deleteFiles($path->getFolder(), true); 101 | } 102 | 103 | // patchEntity etc 104 | ``` 105 | 106 | [< Shell tasks](shell.md) | [FAQ >](faq.md) 107 | -------------------------------------------------------------------------------- /docs/examples/UploadAndDeleteImageListener.md: -------------------------------------------------------------------------------- 1 | ## Customizing behavior of file creation/deletion using event listener 2 | 3 | You can hook Proffer's image creation/deletion as below. 4 | 5 | ### Create src/Event/UploadAndDeleteImageListener.php 6 | 7 | ```php 8 | 'createImage', 23 | 'Proffer.beforeDeleteImage' => 'deleteImage', 24 | ]; 25 | } 26 | 27 | public function createImage(Event $event, ProfferPath $path, $imagePath) 28 | { 29 | Log::write('debug', 'hook event of createImage path: ' . $imagePath); 30 | 31 | // copy file to external service (e.g. Amazon S3) 32 | // delete locale file 33 | } 34 | 35 | public function deleteImage(Event $event, ProfferPath $path) 36 | { 37 | Log::write('debug', 'hook event of deleteImage folder: ' . $path->getFolder()); 38 | 39 | // delete file from external service (e.g. Amazon S3) 40 | } 41 | } 42 | ``` 43 | 44 | ### Register listener to EventManager in config/bootstrap.php 45 | 46 | ```php 47 | Cake\Event\EventManager::instance()->on(new \App\Event\UploadAndDeleteImageListener()); 48 | ``` 49 | -------------------------------------------------------------------------------- /docs/examples/UploadFilenameListener.md: -------------------------------------------------------------------------------- 1 | ```php 2 | 13 | * @when 03/03/15 14 | * 15 | */ 16 | 17 | namespace App\Event; 18 | 19 | use Cake\Event\Event; 20 | use Cake\Event\EventListenerInterface; 21 | use Cake\Utility\Inflector; 22 | use Proffer\Lib\ProfferPath; 23 | 24 | class UploadFilenameListener implements EventListenerInterface 25 | { 26 | public function implementedEvents() 27 | { 28 | return [ 29 | 'Proffer.afterPath' => 'change', 30 | ]; 31 | } 32 | 33 | /** 34 | * Rename a file and change it's upload folder before it's processed 35 | * 36 | * @param Event $event The event class with a subject of the entity 37 | * @param ProfferPath $path 38 | * @return ProfferPath $path 39 | */ 40 | public function change(Event $event, ProfferPath $path) 41 | { 42 | // Detect and select the right file extension 43 | switch ($event->subject()->get('image')['type']) { 44 | default: 45 | case "image/jpeg": 46 | $ext = '.jpg'; 47 | break; 48 | case "image/png": 49 | $ext = '.png'; 50 | break; 51 | case "image/gif": 52 | $ext = '.gif'; 53 | break; 54 | } 55 | 56 | // Create a new filename using the id and the name of the entity 57 | $newFilename = $event->subject()->get('id') . '_' . Inflector::slug($event->subject()->get('name')) . $ext; 58 | 59 | // This would set the containing upload folder to `webroot/files/user_profile_pictures///` 60 | // for every file uploaded through the table this listener was attached to. 61 | $path->setTable('user_profile_pictures'); 62 | 63 | // If a seed is set in the data already, we'll use that rather than make a new one each time we upload 64 | if (empty($event->subject()->get('image_dir'))) { 65 | $path->setSeed(date('Y-m-d-His')); 66 | } 67 | 68 | // Change the filename in both the path to be saved, and in the entity data for saving to the db 69 | $path->setFilename($newFilename); 70 | $event->subject('image')['name'] = $newFilename; 71 | 72 | // Must return the modified path instance, so that things are saved in the right place 73 | return $path; 74 | } 75 | } 76 | ``` 77 | -------------------------------------------------------------------------------- /docs/faq.md: -------------------------------------------------------------------------------- 1 | # Frequently asked questions 2 | This manual page collects together all the frequent questions about the plugin, it's functionality and some of the more 3 | common errors people might experience. 4 | 5 | ## Proffers scope 6 | The scope of the plugin is the limit of the functionality it will provide. 7 | 8 | First and foremost it is an upload plugin. This means it's core responsibility is to copy files from once place to 9 | another. Which, in most cases, will be from a client machine to a server. 10 | 11 | Additional functionality to this is the generation of various sizes of thumbnail and some associated tools. In this 12 | capacity there are some events which process images and create the thumbnails. There are also some related shell tasks 13 | to make thumbnail generation easier. 14 | 15 | Some things which the plugin does not do are provide methods for linking images in the front-end of your website, such 16 | 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 17 | interact with your admin to display uploaded images or anything like that. 18 | 19 | Proffer will also not manage your file system for you. It can only upload images, and doesn't version them or anything 20 | similar. This kind of functionality would need to be developed by the developer. 21 | 22 | The provided thumbnail generation is basic. If you want to expand upon this, such as creating new types of thumbnail or 23 | creating watermarked images you are encouraged to hook the events in the plugin and create your own code for generating 24 | your customised thumbnails. 25 | 26 | ## Errors 27 | If you are experiencing any of these errors, here are your solutions. 28 | 29 | ### Bootstrap file is missing 30 | Proffer `0.5.0` introduced configuring schema settings automatically, so you no longer need to include the the 31 | `['bootstrap' => true]` when loading the plugin. 32 | 33 | ### File name is written to the database as "Array" 34 | The thing to check is your form is using the file type, and your input is also a file type. 35 | 36 | ```php 37 | echo $this->Form->input($entity, ['type' => 'file']); 38 | echo $this->Form->input('file_upload', ['type' => 'file']); 39 | // etc 40 | ``` 41 | ### No database changes and no file system changes 42 | If the form is submitting without issue, yet no file upload is tacking place, ensure that your form is multipart. In your template, make sure your form is type file. `$this->Form->create($example, ['type' => 'file'])`. 43 | 44 | ## Still having trouble? 45 | If you're still having trouble, head to `#cakephp` on Freenode.net and ask for help. A web chat client is available 46 | on [the Freenode website](http://webchat.freenode.net/). 47 | 48 | 49 | [< Examples](examples.md) | [Readme >](../README.md) 50 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | This manual page deals with the installation of the Proffer plugin. Where you can get the code and where should it be in your project. 3 | 4 | ## Packagist 5 | You can find it on Packagist [https://packagist.org/packages/davidyell/proffer](https://packagist.org/packages/davidyell/proffer) 6 | 7 | ## Getting the plugin 8 | In your terminal you can use 9 | 10 | ```bash 11 | $ composer require 'davidyell/proffer:^0.8' 12 | ``` 13 | 14 | It's always advised to lock your dependencies to a specific version number. You can [check the releases](https://github.com/davidyell/CakePHP3-Proffer/releases), 15 | or [read more about versions on Composer.org](https://getcomposer.org/doc/01-basic-usage.md#package-versions). For more information about [installing plugins with CakePHP](http://book.cakephp.org/3.0/en/plugins.html#installing-a-plugin-with-composer), check the book. 16 | 17 | :warning: Installing the plugin without the use of Composer is unsupported, you do so at your own risk. 18 | 19 | ## CakePHP 20 | Then you'll need to load the plugin in your `src/Application.php` file. 21 | 22 | ```php 23 | $this->addPlugin('Proffer'); 24 | ``` 25 | 26 | or you can use the console to do this for you. 27 | 28 | ```bash 29 | bin/cake plugin load Proffer 30 | ``` 31 | 32 | ## Database 33 | 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 34 | uploaded file such as `example.jpg` and you also need the dir field to store the directory in which the file has been 35 | stored. By default this is `dir`. 36 | 37 | An example query to add columns might look like this for MySQL. 38 | 39 | ```sql 40 | ALTER TABLE `teams` 41 | ADD COLUMN `photo` VARCHAR(255), 42 | ADD COLUMN `photo_dir` VARCHAR(255) 43 | ``` 44 | 45 | Don't forget to ensure that the fields are present in your entities `$_accessible` array. 46 | 47 | [< Readme](../README.md) | [Configuration >](configuration.md) 48 | -------------------------------------------------------------------------------- /docs/shell.md: -------------------------------------------------------------------------------- 1 | # Proffer shell tasks 2 | This manual page deals with the command line tools which are included with the Proffer plugin. 3 | 4 | ## Getting available tasks 5 | Proffer comes with a built in shell which can help you achieve certain things when dealing with your uploaded files. To 6 | find out more about the shell you can use the following command to output the help and options. 7 | 8 | ```bash 9 | $ bin/cake proffer 10 | ``` 11 | 12 | ## Regenerate thumbnail task 13 | If you would like to regenerate the thumbnails for files already on your system, or you've changed your configuration. You 14 | can use the built-in shell to regenerate the thumbnails for a table. 15 | 16 | ```bash 17 | $ bin/cake proffer generate
18 | $ bin/cake proffer.proffer generate .
19 | ``` 20 | 21 | If you have used a custom ImageTransform or Path class in your uploads, these can be passed as params. 22 | This example shows regenerating thumbnails for the `UserImages` table class, using a custom path class. 23 | **Note** the fully namespaced class name and escaped double backslash. 24 | 25 | ```bash 26 | $ bin/cake proffer generate -p \\App\\Lib\\Proffer\\UserImagePath UserImages 27 | ``` 28 | 29 | ## Cleanup task 30 | The cleanup task will look at a model's uploads folder and match the files there with its matching entry in the 31 | database. If a file doesn't have a matching record in the database it **will be deleted**. 32 | 33 | :warning: This shell only works with the default behaviour settings. 34 | 35 | ```bash 36 | $ bin/cake proffer cleanup -vd
37 | ``` 38 | 39 | Using the `-vd` options will perform a verbose dry-run, this is recommended before running the shell for real. 40 | 41 | [< Customisation](customisation.md) | [Examples >](examples.md) 42 | -------------------------------------------------------------------------------- /docs/upgrading.md: -------------------------------------------------------------------------------- 1 | # Upgrading 2 | If you are upgrading between versions this documentation page will give you some insights into the changes which you 3 | will need to make and potential pitfalls. 4 | 5 | For more release information [please see the releases](https://github.com/davidyell/CakePHP3-Proffer/releases). 6 | 7 | ## 0.7.0 8 | [Release 0.7.0](https://github.com/davidyell/CakePHP3-Proffer/releases/tag/0.7.0) 9 | 10 | You should only encounter problems if you have a Transform class which depends upon the Imagine image library, which has been removed in this release. 11 | 12 | ## 0.6.0 13 | [Release 0.6.0](https://github.com/davidyell/CakePHP3-Proffer/releases/tag/0.6.0) 14 | 15 | When migrating to `0.6.0` you might encounter problems with validation, specifically the `filesize()` method. You will 16 | need to change the param order to match, `fileSize($check, $operator = null, $size = null)`. This is documented in the 17 | [api validation docs](http://api.cakephp.org/3.0/class-Cake.Validation.Validation.html#_fileSize). 18 | 19 | The `operator` can be either a word or operand is greater >, is less <, greater or equal >= less or equal <=, is less <, 20 | equal to ==, not equal !=. 21 | 22 | ## 0.5.0 23 | [Release 0.5.0](https://github.com/davidyell/CakePHP3-Proffer/tree/0.5.0) 24 | 25 | When upgrading to `0.5.0` you no longer need to bootstrap the plugin, as the data type class will be loaded 26 | automatically. 27 | 28 | So the only change required is to change your `config/bootstrap.php` to be `Plugin::load('Proffer')`. 29 | 30 | ## 0.4.0 31 | [Release 0.4.0](https://github.com/davidyell/CakePHP3-Proffer/releases/tag/v0.4.0) 32 | 33 | This version removes some of the events in the plugin, so any code which hooks the events will need to be updated. 34 | Instead of hooking these events you can inject your own transform class in the plugin in which you can implement your 35 | changes. 36 | 37 | ## 0.3.0 38 | [Release 0.3.0](https://github.com/davidyell/CakePHP3-Proffer/releases/tag/v0.3.0) 39 | 40 | If you need to make the generation of thumbnails optional, this is now possible by updating the configuration. 41 | -------------------------------------------------------------------------------- /docs/validation.md: -------------------------------------------------------------------------------- 1 | # Validation 2 | This manual page deals with how to use the included ProfferRules validation provider to add upload related validation rules to 3 | your application. 4 | 5 | ## Basic validation 6 | If you bake your Table class, be aware that Bake will add some basic string validation for your upload field, because the file name is stored as a string. 7 | 8 | You might see some rules like the following, depending on your CakePHP version. You might want to remove these as the request data will be a file and not a string until after the behaviour has run. 9 | ```php 10 | $validator 11 | ->scalar('photo') 12 | ->maxLength('photo', 255) 13 | ->allowEmptyString('photo'); 14 | ``` 15 | 16 | ## Built-in validation provider 17 | Proffer comes an extra validation rule to check the dimensions of an uploaded image. Other rules are provided by the core and are listed below. 18 | 19 | In your validation function in your table class you'll need to add the validator as a provider and then apply the rules. 20 | 21 | ```php 22 | provider('proffer', 'Proffer\Model\Validation\ProfferRules'); 24 | 25 | // Set the thumbnail resize dimensions 26 | ->add('photo', 'proffer', [ 27 | 'rule' => ['dimensions', [ 28 | 'min' => ['w' => 100, 'h' => 100], 29 | 'max' => ['w' => 500, 'h' => 500] 30 | ]], 31 | 'message' => 'Image is not correct dimensions.', 32 | 'provider' => 'proffer' 33 | ]); 34 | ``` 35 | 36 | You can [read more about custom validation providers in the book](http://book.cakephp.org/3.0/en/core-libraries/validation.html#adding-validation-providers). 37 | 38 | If you need to validate other aspects of the uploaded file, there are a number of core validation methods you might find helpful. 39 | * [Extension](http://api.cakephp.org/3.0/class-Cake.Validation.Validation.html#_extension) 40 | * [File size](http://api.cakephp.org/3.0/class-Cake.Validation.Validation.html#_fileSize) 41 | * [Mime type](http://api.cakephp.org/3.0/class-Cake.Validation.Validation.html#_mimeType) 42 | 43 | ## Basic validation rules 44 | If you want your users to submit a file when creating a record, but not when updating it, you can configure this using the basic Cake rules. 45 | 46 | ```php 47 | $validator 48 | ->requirePresence('photo', 'create') 49 | ->allowEmpty('photo', 'update'); 50 | ``` 51 | 52 | So now your users do not need to upload a file every time they update a record. 53 | 54 | [< Configuration](configuration.md) | [Customisation >](customisation.md) 55 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | autoload_files: 3 | - tests/bootstrap.php 4 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 15 | ./src 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ./tests/TestCase 28 | 29 | 30 | 31 | 32 | 33 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/Database/Type/FileType.php: -------------------------------------------------------------------------------- 1 | 8 | * @when 03/03/15 9 | * 10 | */ 11 | 12 | namespace Proffer\Database\Type; 13 | 14 | use Cake\Database\Type\StringType; 15 | 16 | class FileType extends StringType 17 | { 18 | /** 19 | * Prevent the marhsaller changing the upload array into a string 20 | * 21 | * @param mixed $value Passed upload array 22 | * @return mixed 23 | */ 24 | public function marshal($value) 25 | { 26 | return $value; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Exception/CannotUploadFileException.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Proffer\Exception; 10 | 11 | class CannotUploadFileException extends \Exception 12 | { 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/Exception/InvalidClassException.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Proffer\Exception; 10 | 11 | use Exception; 12 | 13 | class InvalidClassException extends Exception 14 | { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/Lib/ImageTransform.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Proffer\Lib; 10 | 11 | use Cake\ORM\Table; 12 | use Intervention\Image\Image; 13 | use Intervention\Image\ImageManager; 14 | 15 | class ImageTransform implements ImageTransformInterface 16 | { 17 | 18 | /** 19 | * @var \Cake\ORM\Table $table Instance of the table being used 20 | */ 21 | protected $Table; 22 | 23 | /** 24 | * @var \Proffer\Lib\ProfferPathInterface $Path Instance of the path class 25 | */ 26 | protected $Path; 27 | 28 | /** 29 | * @var \Intervention\Image\ImageManager Intervention image manager instance 30 | */ 31 | protected $ImageManager; 32 | 33 | /** 34 | * Construct the transformation class 35 | * 36 | * @param \Cake\ORM\Table $table The table instance 37 | * @param \Proffer\Lib\ProfferPathInterface $path Instance of the path class 38 | */ 39 | public function __construct(Table $table, ProfferPathInterface $path) 40 | { 41 | $this->Table = $table; 42 | $this->Path = $path; 43 | } 44 | 45 | /** 46 | * Take an upload fields configuration and create all the thumbnails 47 | * 48 | * @param array $config The upload fields configuration 49 | * @return array 50 | */ 51 | public function processThumbnails(array $config) 52 | { 53 | $thumbnailPaths = []; 54 | if (!isset($config['thumbnailSizes'])) { 55 | return $thumbnailPaths; 56 | } 57 | 58 | foreach ($config['thumbnailSizes'] as $prefix => $thumbnailConfig) { 59 | $method = 'gd'; 60 | if (!empty($config['thumbnailMethod'])) { 61 | $method = $config['thumbnailMethod']; 62 | } 63 | 64 | $this->ImageManager = new ImageManager(['driver' => $method]); 65 | 66 | $thumbnailPaths[] = $this->makeThumbnail($prefix, $thumbnailConfig); 67 | } 68 | 69 | return $thumbnailPaths; 70 | } 71 | 72 | /** 73 | * Generate and save the thumbnail 74 | * 75 | * @param string $prefix The thumbnail prefix 76 | * @param array $config Array of thumbnail config 77 | * @return string 78 | */ 79 | public function makeThumbnail($prefix, array $config) 80 | { 81 | $defaultConfig = [ 82 | 'jpeg_quality' => 100 83 | ]; 84 | $config = array_merge($defaultConfig, $config); 85 | 86 | $width = !empty($config['w']) ? $config['w'] : null; 87 | $height = !empty($config['h']) ? $config['h'] : null; 88 | 89 | $image = $this->ImageManager->make($this->Path->fullPath()); 90 | 91 | if (!empty($config['orientate'])) { 92 | $image = $this->orientate($image); 93 | } 94 | 95 | if (!empty($config['crop'])) { 96 | $image = $this->thumbnailCrop($image, $width, $height); 97 | } elseif (!empty($config['fit'])) { 98 | $image = $this->thumbnailFit($image, $width, $height); 99 | } elseif (!empty($config['custom'])) { 100 | $image = $this->thumbnailCustom($image, $config['custom'], $config['params']); 101 | } else { 102 | $image = $this->thumbnailResize($image, $width, $height); 103 | } 104 | 105 | unset($config['crop'], $config['w'], $config['h'], $config['custom'], $config['params'], $config['orientate']); 106 | 107 | $image->save($this->Path->fullPath($prefix), $config['jpeg_quality']); 108 | 109 | return $this->Path->fullPath($prefix); 110 | } 111 | 112 | /** 113 | * Crop an image to a certain size from the centre of the image 114 | * 115 | * @see http://image.intervention.io/api/crop 116 | * 117 | * @param \Intervention\Image\Image $image Image instance 118 | * @param int $width Desired width in pixels 119 | * @param int $height Desired height in pixels 120 | * 121 | * @return \Intervention\Image\Image 122 | */ 123 | protected function thumbnailCrop(Image $image, $width, $height) 124 | { 125 | return $image->crop($width, $height); 126 | } 127 | 128 | /** 129 | * Resize and crop to find the best fitting aspect ratio 130 | * 131 | * @see http://image.intervention.io/api/fit 132 | * 133 | * @param \Intervention\Image\Image $image Image instance 134 | * @param int $width Desired width in pixels 135 | * @param int $height Desired height in pixels 136 | * 137 | * @return \Intervention\Image\Image 138 | */ 139 | protected function thumbnailFit(Image $image, $width, $height) 140 | { 141 | return $image->fit($width, $height); 142 | } 143 | 144 | /** 145 | * Resize current image 146 | * 147 | * @see http://image.intervention.io/api/resize 148 | * 149 | * @param \Intervention\Image\Image $image Image instance 150 | * @param int $width Desired width in pixels 151 | * @param int $height Desired height in pixels 152 | * 153 | * @return \Intervention\Image\Image 154 | */ 155 | protected function thumbnailResize(Image $image, $width, $height) 156 | { 157 | return $image->resize($width, $height, function ($constraint) { 158 | $constraint->aspectRatio(); 159 | }); 160 | } 161 | 162 | /** 163 | * Call any method from the intervention library 164 | * 165 | * @see http://image.intervention.io/ 166 | * 167 | * @param \Intervention\Image\Image $image Image instance 168 | * @param string $custom Method you want to call 169 | * @param array $params Array of parameters to pass to the method 170 | * 171 | * @return \Intervention\Image\Image 172 | */ 173 | protected function thumbnailCustom(Image $image, $custom, $params) 174 | { 175 | if (method_exists($image, $custom)) { 176 | return call_user_func_array($image->{$custom}(), $params); 177 | } 178 | 179 | return $image; 180 | } 181 | 182 | /** 183 | * EXIF orientate the current image 184 | * 185 | * @see http://image.intervention.io/api/orientate 186 | * 187 | * @param \Intervention\Image\Image $image Image instance 188 | * @return \Intervention\Image\Image 189 | */ 190 | protected function orientate(Image $image) 191 | { 192 | return $image->orientate(); 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/Lib/ImageTransformInterface.php: -------------------------------------------------------------------------------- 1 | 8 | * @when 23/03/15 9 | * 10 | */ 11 | 12 | namespace Proffer\Lib; 13 | 14 | interface ImageTransformInterface 15 | { 16 | /** 17 | * Take an upload fields configuration and process each configured thumbnail 18 | * 19 | * @param array $config The upload fields configuration 20 | * @return array 21 | */ 22 | public function processThumbnails(array $config); 23 | 24 | /** 25 | * Create a thumbnail from a source file 26 | * 27 | * @param string $prefix The prefix name for the thumbnail 28 | * @param array $dimensions The thumbnail dimensions 29 | * @return string 30 | */ 31 | public function makeThumbnail($prefix, array $dimensions); 32 | } 33 | -------------------------------------------------------------------------------- /src/Lib/ProfferPath.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Proffer\Lib; 10 | 11 | use Cake\Datasource\EntityInterface; 12 | use Cake\ORM\Table; 13 | use Cake\Utility\Text; 14 | 15 | class ProfferPath implements ProfferPathInterface 16 | { 17 | 18 | protected $root; 19 | 20 | protected $table; 21 | 22 | protected $field; 23 | 24 | protected $seed; 25 | 26 | protected $filename; 27 | 28 | protected $prefixes = []; 29 | 30 | /** 31 | * Construct the class and setup the defaults 32 | * 33 | * @param Table $table Instance of the table 34 | * @param EntityInterface $entity Instance of the entity data 35 | * @param string $field The name of the upload field 36 | * @param array $settings Array of settings for the upload field 37 | */ 38 | public function __construct(Table $table, EntityInterface $entity, $field, array $settings) 39 | { 40 | if (isset($settings['root'])) { 41 | $this->setRoot($settings['root']); 42 | } else { 43 | $this->setRoot(WWW_ROOT . 'files'); 44 | } 45 | 46 | $this->setTable($table->getAlias()); 47 | $this->setField($field); 48 | $this->setSeed($this->generateSeed($entity->get($settings['dir']))); 49 | 50 | if (isset($settings['thumbnailSizes'])) { 51 | $this->setPrefixes($settings['thumbnailSizes']); 52 | } 53 | 54 | $this->setFilename($entity->get($field)); 55 | } 56 | 57 | /** 58 | * Get the root 59 | * 60 | * @return string 61 | */ 62 | public function getRoot() 63 | { 64 | return $this->root; 65 | } 66 | 67 | /** 68 | * Set the root 69 | * 70 | * @param string $root The absolute path to the root of your upload folder, all 71 | * files will be uploaded under this path. 72 | * @return void 73 | */ 74 | public function setRoot($root) 75 | { 76 | $this->root = $root; 77 | } 78 | 79 | /** 80 | * Get the table 81 | * 82 | * @return string 83 | */ 84 | public function getTable() 85 | { 86 | return $this->table; 87 | } 88 | 89 | /** 90 | * Set the table 91 | * 92 | * @param string $table The name of the table the behaviour is dealing with. 93 | * @return void 94 | */ 95 | public function setTable($table) 96 | { 97 | $this->table = strtolower($table); 98 | } 99 | 100 | /** 101 | * Get the field 102 | * 103 | * @return string 104 | */ 105 | public function getField() 106 | { 107 | return $this->field; 108 | } 109 | 110 | /** 111 | * Set the field 112 | * 113 | * @param string $field The name of the upload field 114 | * @return void 115 | */ 116 | public function setField($field) 117 | { 118 | $this->field = $field; 119 | } 120 | 121 | /** 122 | * Get the seed 123 | * 124 | * @return string 125 | */ 126 | public function getSeed() 127 | { 128 | return $this->seed; 129 | } 130 | 131 | /** 132 | * Set the seed 133 | * 134 | * @param string $seed The seed string used to create a folder for the uploaded files 135 | * @return void 136 | */ 137 | public function setSeed($seed) 138 | { 139 | $this->seed = $seed; 140 | } 141 | 142 | /** 143 | * Get the filename 144 | * 145 | * @return string 146 | */ 147 | public function getFilename() 148 | { 149 | return $this->filename; 150 | } 151 | 152 | /** 153 | * Set the filename or pull it from the upload array 154 | * 155 | * @param string|array $filename The name of the actual file including it's extension 156 | * @return void 157 | */ 158 | public function setFilename($filename) 159 | { 160 | if (is_array($filename) && isset($filename['name'])) { 161 | $this->filename = $filename['name']; 162 | } else { 163 | $this->filename = $filename; 164 | } 165 | } 166 | 167 | /** 168 | * Get all the thumbnail size prefixes 169 | * 170 | * @return array 171 | */ 172 | public function getPrefixes() 173 | { 174 | return $this->prefixes; 175 | } 176 | 177 | /** 178 | * Take the configured thumbnail sizes and store the prefixes 179 | * 180 | * @param array $thumbnailSizes The 'thumbnailSizes' dimension of the behaviour configuration array 181 | * @return void 182 | */ 183 | public function setPrefixes(array $thumbnailSizes) 184 | { 185 | foreach ($thumbnailSizes as $prefix => $dimensions) { 186 | array_push($this->prefixes, $prefix); 187 | } 188 | } 189 | 190 | /** 191 | * Create a path seed value. 192 | * 193 | * @param string $seed The current seed if there is one 194 | * @return string 195 | */ 196 | public function generateSeed($seed = null) 197 | { 198 | if ($seed) { 199 | return $seed; 200 | } 201 | 202 | return Text::uuid(); 203 | } 204 | 205 | /** 206 | * Return the complete absolute path to an upload. If it's an image with thumbnails you can pass the prefix to 207 | * get the path to the prefixed thumbnail file. 208 | * 209 | * @param string $prefix Thumbnail prefix 210 | * @return string 211 | */ 212 | public function fullPath($prefix = null) 213 | { 214 | if ($prefix) { 215 | return $this->getFolder() . $prefix . '_' . $this->getFilename(); 216 | } 217 | 218 | return $this->getFolder() . $this->getFilename(); 219 | } 220 | 221 | /** 222 | * Return the absolute path to the containing parent folder where all the files will be uploaded 223 | * 224 | * @return string 225 | */ 226 | public function getFolder() 227 | { 228 | $table = $this->getTable(); 229 | $table = (!empty($table)) ? $table . DS : null; 230 | 231 | $seed = $this->getSeed(); 232 | $seed = (!empty($seed)) ? $seed . DS : null; 233 | 234 | return $this->getRoot() . DS . $table . $this->getField() . DS . $seed; 235 | } 236 | 237 | /** 238 | * Check if the upload folder has already been created and if not create it 239 | * 240 | * @return bool 241 | */ 242 | public function createPathFolder() 243 | { 244 | if (!file_exists($this->getFolder())) { 245 | return mkdir($this->getFolder(), 0777, true); 246 | } 247 | 248 | return true; 249 | } 250 | 251 | /** 252 | * Clear out a folder and optionally delete it 253 | * 254 | * @param string $folder Absolute path to the folder 255 | * @param bool $rmdir If you want to remove the folder as well 256 | * @return bool 257 | */ 258 | public function deleteFiles($folder, $rmdir = false) 259 | { 260 | $fileList = glob($folder . DS . '*'); 261 | if ($fileList !== false) { 262 | array_map('unlink', $fileList); 263 | } 264 | 265 | if ($rmdir) { 266 | rmdir($folder); 267 | } 268 | 269 | return true; 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /src/Lib/ProfferPathInterface.php: -------------------------------------------------------------------------------- 1 | 8 | * @when 23/03/15 9 | * 10 | */ 11 | 12 | namespace Proffer\Lib; 13 | 14 | interface ProfferPathInterface 15 | { 16 | 17 | /** 18 | * Returns the root folder in which all uploads should be placed. 19 | * 20 | * @return string 21 | */ 22 | public function getRoot(); 23 | 24 | /** 25 | * Set the root folder for all uploads. 26 | * Default is WWW_DIR . 'files' 27 | * 28 | * @param string $root The root folder into which all uploads will be placed 29 | * @return void 30 | */ 31 | public function setRoot($root); 32 | 33 | /** 34 | * Returns the name of the table the upload is associated with. 35 | * 36 | * @return string 37 | */ 38 | public function getTable(); 39 | 40 | /** 41 | * Set the table name 42 | * 43 | * @param string $table The name of the table 44 | * @return void 45 | */ 46 | public function setTable($table); 47 | 48 | /** 49 | * Returns the name of the upload field as configured in the table. 50 | * 51 | * @return string 52 | */ 53 | public function getField(); 54 | 55 | /** 56 | * The name of the upload field 57 | * 58 | * @param string $field The upload field 59 | * @return void 60 | */ 61 | public function setField($field); 62 | 63 | /** 64 | * Returns the seed used to generate a folder to hold all the associated uploads. 65 | * Should be the contents of the 'dir' field configured in the Table. 66 | * 67 | * @return string 68 | */ 69 | public function getSeed(); 70 | 71 | /** 72 | * The path seed used to create a folder into which files can be uploaded 73 | * 74 | * @param string $seed The seed value 75 | * @return void 76 | */ 77 | public function setSeed($seed); 78 | 79 | /** 80 | * Return the name of the uploaded file. 81 | * 82 | * @return string 83 | */ 84 | public function getFilename(); 85 | 86 | /** 87 | * The filename for the uploaded file 88 | * 89 | * @param string $filename The name of the file 90 | * @return void 91 | */ 92 | public function setFilename($filename); 93 | 94 | /** 95 | * Returns an array of all the configured prefixes. 96 | * 97 | * @return array 98 | */ 99 | public function getPrefixes(); 100 | 101 | /** 102 | * Set the thumbnail prefixes from the configured thumbnail sizes. 103 | * 104 | * @param array $thumbnailSizes Array of different thumbnail sizes, keyed with the thumbnail prefix 105 | * @return void 106 | */ 107 | public function setPrefixes(array $thumbnailSizes); 108 | 109 | /** 110 | * Will create a new seed for new uploads. Should also pass back existing seed for new uploads to the same record. 111 | * Default return is String::uuid() 112 | * 113 | * @param string $seed The existing seed if one exists 114 | * @return string 115 | */ 116 | public function generateSeed($seed); 117 | 118 | /** 119 | * Return the complete absolute path to an upload. If it's an image with thumbnails you can pass the prefix to 120 | * get the path to the prefixed thumbnail file. 121 | * 122 | * @param string $prefix The specific prefix to get the path for 123 | * @return string 124 | */ 125 | public function fullPath($prefix = null); 126 | 127 | /** 128 | * Return the absolute path to the containing parent folder where all the files will be uploaded 129 | * 130 | * @return string 131 | */ 132 | public function getFolder(); 133 | 134 | /** 135 | * Check if the upload folder has already been created and if not create it 136 | * 137 | * @return bool 138 | */ 139 | public function createPathFolder(); 140 | 141 | /** 142 | * Remove all images from a folder and optionally remove the folder as well 143 | * 144 | * @param string $folder The absolute path to the folder to remove. 145 | * @param bool $rmdir If you want to remove the folder as well as the files. 146 | * @return bool 147 | */ 148 | public function deleteFiles($folder, $rmdir = false); 149 | } 150 | -------------------------------------------------------------------------------- /src/Model/Behavior/ProfferBehavior.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Proffer\Model\Behavior; 10 | 11 | use ArrayObject; 12 | use Cake\Database\Type; 13 | use Cake\Datasource\EntityInterface; 14 | use Cake\Event\Event; 15 | use Cake\ORM\Behavior; 16 | use Proffer\Exception\CannotUploadFileException; 17 | use Proffer\Exception\InvalidClassException; 18 | use Proffer\Lib\ImageTransform; 19 | use Proffer\Lib\ImageTransformInterface; 20 | use Proffer\Lib\ProfferPath; 21 | use Proffer\Lib\ProfferPathInterface; 22 | 23 | /** 24 | * Proffer behavior 25 | */ 26 | class ProfferBehavior extends Behavior 27 | { 28 | /** 29 | * Build the behaviour 30 | * 31 | * @param array $config Passed configuration 32 | * 33 | * @return void 34 | */ 35 | public function initialize(array $config) 36 | { 37 | Type::map('proffer.file', '\Proffer\Database\Type\FileType'); 38 | $schema = $this->_table->getSchema(); 39 | foreach (array_keys($this->getConfig()) as $field) { 40 | if (is_string($field)) { 41 | $schema->setColumnType($field, 'proffer.file'); 42 | } 43 | } 44 | $this->_table->setSchema($schema); 45 | } 46 | 47 | /** 48 | * beforeMarshal event 49 | * 50 | * If a field is allowed to be empty as defined in the validation it should be unset to prevent processing 51 | * 52 | * @param \Cake\Event\Event $event Event instance 53 | * @param ArrayObject $data Data to process 54 | * @param ArrayObject $options Array of options for event 55 | * 56 | * @return void 57 | */ 58 | public function beforeMarshal(Event $event, ArrayObject $data, ArrayObject $options) 59 | { 60 | foreach ($this->getConfig() as $field => $settings) { 61 | if ($this->_table->getValidator()->isEmptyAllowed($field, false) && 62 | isset($data[$field]['error']) && $data[$field]['error'] === UPLOAD_ERR_NO_FILE 63 | ) { 64 | unset($data[$field]); 65 | } 66 | } 67 | } 68 | 69 | /** 70 | * beforeSave method 71 | * 72 | * Hook the beforeSave to process the request data 73 | * 74 | * @param \Cake\Event\Event $event The event 75 | * @param \Cake\Datasource\EntityInterface $entity The entity 76 | * @param ArrayObject $options Array of options 77 | * @param \Proffer\Lib\ProfferPathInterface|null $path Inject an instance of ProfferPath 78 | * 79 | * @return true 80 | * @throws \Exception 81 | */ 82 | public function beforeSave(Event $event, EntityInterface $entity, ArrayObject $options, ProfferPathInterface $path = null) 83 | { 84 | foreach ($this->getConfig() as $field => $settings) { 85 | $tableEntityClass = $this->_table->getEntityClass(); 86 | 87 | if ($entity->has($field) && is_array($entity->get($field)) && $entity->get($field)['error'] === UPLOAD_ERR_OK) { 88 | $this->process($field, $settings, $entity, $path); 89 | } elseif ($tableEntityClass !== null && $entity instanceof $tableEntityClass && $entity->get('error') === UPLOAD_ERR_OK) { 90 | $filename = $entity->get('name'); 91 | $entity->set($field, $filename); 92 | 93 | if (empty($entity->get($settings['dir']))) { 94 | $entity->set($settings['dir'], null); 95 | } 96 | 97 | $this->process($field, $settings, $entity); 98 | } 99 | } 100 | 101 | return true; 102 | } 103 | 104 | /** 105 | * Process any uploaded files, generate paths, move the files and kick off thumbnail generation if it's an image 106 | * 107 | * @param string $field The upload field name 108 | * @param array $settings Array of upload settings for the field 109 | * @param \Cake\Datasource\EntityInterface $entity The current entity to process 110 | * @param \Proffer\Lib\ProfferPathInterface|null $path Inject an instance of ProfferPath 111 | * 112 | * @return void 113 | * @throws \Exception If the file cannot be renamed / moved to the correct path 114 | */ 115 | protected function process($field, array $settings, EntityInterface $entity, ProfferPathInterface $path = null) 116 | { 117 | $path = $this->createPath($entity, $field, $settings, $path); 118 | 119 | if (is_array($entity->get($field)) && count(array_filter(array_keys($entity->get($field)), 'is_string')) > 0) { 120 | $uploadList = [$entity->get($field)]; 121 | } else { 122 | $uploadList = [ 123 | [ 124 | 'name' => $entity->get('name'), 125 | 'type' => $entity->get('type'), 126 | 'tmp_name' => $entity->get('tmp_name'), 127 | 'error' => $entity->get('error'), 128 | 'size' => $entity->get('size'), 129 | ] 130 | ]; 131 | } 132 | 133 | foreach ($uploadList as $upload) { 134 | if ($this->moveUploadedFile($upload['tmp_name'], $path->fullPath())) { 135 | $entity->set($field, $path->getFilename()); 136 | $entity->set($settings['dir'], $path->getSeed()); 137 | 138 | $this->createThumbnails($entity, $settings, $path); 139 | } else { 140 | throw new CannotUploadFileException("File `{$upload['name']}` could not be copied."); 141 | } 142 | } 143 | 144 | unset($path); 145 | } 146 | 147 | /** 148 | * Load a path class instance and create the path for the uploads to be moved into 149 | * 150 | * @param \Cake\Datasource\EntityInterface $entity Instance of the entity 151 | * @param string $field The upload field name 152 | * @param array $settings Array of upload settings for the field 153 | * @param \Proffer\Lib\ProfferPathInterface|null $path Inject an instance of ProfferPath 154 | * 155 | * @return \Proffer\Lib\ProfferPathInterface 156 | * @throws \Proffer\Exception\InvalidClassException If the custom class doesn't implement the interface 157 | */ 158 | protected function createPath(EntityInterface $entity, $field, array $settings, ProfferPathInterface $path = null) 159 | { 160 | if (!empty($settings['pathClass'])) { 161 | $path = new $settings['pathClass']($this->_table, $entity, $field, $settings); 162 | if (!$path instanceof ProfferPathInterface) { 163 | throw new InvalidClassException("Class {$settings['pathClass']} does not implement the ProfferPathInterface."); 164 | } 165 | } elseif (!isset($path)) { 166 | $path = new ProfferPath($this->_table, $entity, $field, $settings); 167 | } 168 | 169 | $event = new Event('Proffer.afterPath', $entity, ['path' => $path]); 170 | $this->_table->getEventManager()->dispatch($event); 171 | if (!empty($event->result)) { 172 | $path = $event->result; 173 | } 174 | 175 | $path->createPathFolder(); 176 | 177 | return $path; 178 | } 179 | 180 | /** 181 | * Create a new image transform instance, and create any configured thumbnails; if the upload is an image and there 182 | * are thumbnails configured. 183 | * 184 | * @param \Cake\Datasource\EntityInterface $entity Instance of the entity 185 | * @param array $settings Array of upload field settings 186 | * @param \Proffer\Lib\ProfferPathInterface $path Instance of the path class 187 | * 188 | * @return void 189 | * @throws \Proffer\Exception\InvalidClassException If the transform class doesn't implement the interface 190 | */ 191 | protected function createThumbnails(EntityInterface $entity, array $settings, ProfferPathInterface $path) 192 | { 193 | if (getimagesize($path->fullPath()) !== false && isset($settings['thumbnailSizes'])) { 194 | $imagePaths = [$path->fullPath()]; 195 | 196 | if (!empty($settings['transformClass'])) { 197 | $imageTransform = new $settings['transformClass']($this->_table, $path); 198 | if (!$imageTransform instanceof ImageTransformInterface) { 199 | throw new InvalidClassException("Class {$settings['pathClass']} does not implement the ImageTransformInterface."); 200 | } 201 | } else { 202 | $imageTransform = new ImageTransform($this->_table, $path); 203 | } 204 | 205 | $thumbnailPaths = $imageTransform->processThumbnails($settings); 206 | $imagePaths = array_merge($imagePaths, $thumbnailPaths); 207 | 208 | $eventData = ['path' => $path, 'images' => $imagePaths]; 209 | $event = new Event('Proffer.afterCreateImage', $entity, $eventData); 210 | $this->_table->getEventManager()->dispatch($event); 211 | } 212 | } 213 | 214 | /** 215 | * afterDelete method 216 | * 217 | * Remove images from records which have been deleted, if they exist 218 | * 219 | * @param \Cake\Event\Event $event The passed event 220 | * @param \Cake\Datasource\EntityInterface $entity The entity 221 | * @param ArrayObject $options Array of options 222 | * @param \Proffer\Lib\ProfferPathInterface $path Inject an instance of ProfferPath 223 | * 224 | * @return true 225 | */ 226 | public function afterDelete(Event $event, EntityInterface $entity, ArrayObject $options, ProfferPathInterface $path = null) 227 | { 228 | foreach ($this->getConfig() as $field => $settings) { 229 | $dir = $entity->get($settings['dir']); 230 | 231 | if (!empty($entity) && !empty($dir)) { 232 | if (!empty($settings['pathClass'])) { 233 | $path = new $settings['pathClass']($this->_table, $entity, $field, $settings); 234 | } elseif (!isset($path)) { 235 | $path = new ProfferPath($this->_table, $entity, $field, $settings); 236 | } 237 | 238 | $event = new Event('Proffer.beforeDeleteFolder', $entity, ['path' => $path]); 239 | $this->_table->getEventManager()->dispatch($event); 240 | $path->deleteFiles($path->getFolder(), true); 241 | } 242 | 243 | $path = null; 244 | } 245 | 246 | return true; 247 | } 248 | 249 | /** 250 | * Wrapper method for move_uploaded_file to facilitate testing and 'uploading' of local files 251 | * 252 | * This will check if the file has been uploaded or not before picking the correct method to move the file 253 | * 254 | * @param string $file Path to the uploaded file 255 | * @param string $destination The destination file name 256 | * 257 | * @return bool 258 | */ 259 | protected function moveUploadedFile($file, $destination) 260 | { 261 | if (is_uploaded_file($file)) { 262 | return move_uploaded_file($file, $destination); 263 | } 264 | 265 | return rename($file, $destination); 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /src/Model/Validation/ProfferRules.php: -------------------------------------------------------------------------------- 1 | 6 | */ 7 | 8 | namespace Proffer\Model\Validation; 9 | 10 | use Cake\Validation\Validation; 11 | 12 | class ProfferRules extends Validation 13 | { 14 | 15 | /** 16 | * Validate the dimensions of an image. If the file isn't an image then validation will fail 17 | * 18 | * @param array $value An array of the name and value of the field 19 | * @param array $dimensions Array of rule dimensions for example 20 | * ['dimensions', [ 21 | * 'min' => ['w' => 100, 'h' => 100], 22 | * 'max' => ['w' => 500, 'h' => 500] 23 | * ]] 24 | * would validate a minimum size of 100x100 pixels and a maximum of 500x500 pixels 25 | * @return bool 26 | */ 27 | public static function dimensions($value, array $dimensions) 28 | { 29 | $fileDimensions = getimagesize($value['tmp_name']); 30 | 31 | if ($fileDimensions === false) { 32 | return false; 33 | } 34 | 35 | $sourceWidth = $fileDimensions[0]; 36 | $sourceHeight = $fileDimensions[1]; 37 | 38 | foreach ($dimensions as $rule => $sizes) { 39 | if ($rule === 'min') { 40 | if (isset($sizes['w']) && $sourceWidth < $sizes['w']) { 41 | return false; 42 | } 43 | if (isset($sizes['h']) && $sourceHeight < $sizes['h']) { 44 | return false; 45 | } 46 | } elseif ($rule === 'max') { 47 | if (isset($sizes['w']) && $sourceWidth > $sizes['w']) { 48 | return false; 49 | } 50 | if (isset($sizes['h']) && $sourceHeight > $sizes['h']) { 51 | return false; 52 | } 53 | } 54 | } 55 | 56 | return true; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Plugin.php: -------------------------------------------------------------------------------- 1 | 8 | * @when 11/06/18 9 | * 10 | */ 11 | 12 | namespace Proffer; 13 | 14 | use Cake\Core\BasePlugin; 15 | 16 | /** 17 | * Default Plugin class 18 | */ 19 | class Plugin extends BasePlugin 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /src/Shell/ProfferShell.php: -------------------------------------------------------------------------------- 1 | addSubcommand('generate', [ 33 | 'help' => __('Regenerate thumbnails for a specific table.'), 34 | 'parser' => [ 35 | 'description' => [__('Use this command to regenerate the thumbnails for a specific table.')], 36 | 'arguments' => [ 37 | 'table' => ['help' => __('The table to regenerate thumbs for'), 'required' => true] 38 | ], 39 | 'options' => [ 40 | 'path-class' => [ 41 | 'short' => 'p', 42 | 'help' => __('Fully name spaced custom path class, you must use double backslash.') 43 | ], 44 | 'image-class' => [ 45 | 'short' => 'i', 46 | 'help' => __('Fully name spaced custom image transform class, you must use double backslash.') 47 | ], 48 | 'remove-behaviors' => [ 49 | 'help' => __('The behaviors to remove before generate.'), 50 | ], 51 | ] 52 | ] 53 | ]); 54 | $parser->addSubcommand('cleanup', [ 55 | 'help' => __('Clean up old images on the file system which are not linked in the database.'), 56 | 'parser' => [ 57 | 'description' => [__('This command will delete images which are not part of the model configuration.')], 58 | 'arguments' => [ 59 | 'table' => ['help' => __('The table to regenerate thumbs for'), 'required' => true] 60 | ], 61 | 'options' => [ 62 | 'dry-run' => [ 63 | 'short' => 'd', 64 | 'help' => __('Do a dry run and don\'t delete any files.'), 65 | 'boolean' => true 66 | ], 67 | 'remove-behaviors' => [ 68 | 'help' => __('The behaviors to remove before cleanup.'), 69 | ], 70 | ] 71 | ], 72 | ]); 73 | 74 | return $parser; 75 | } 76 | 77 | /** 78 | * Introduction to the shell 79 | * 80 | * @return bool|int|null 81 | */ 82 | public function main() 83 | { 84 | $this->out('Welcome to the Proffer shell.'); 85 | $this->out('This shell can be used to regenerate thumbnails and cleanup unlinked images.'); 86 | $this->hr(); 87 | $this->out($this->OptionParser->help()); 88 | 89 | return parent::main(); 90 | } 91 | 92 | /** 93 | * Load a table, get it's config and then regenerate the thumbnails for that tables upload fields. 94 | * 95 | * @param string $table The name of the table 96 | * @return void 97 | */ 98 | public function generate($table) 99 | { 100 | $this->checkTable($table); 101 | 102 | $config = $this->Table->behaviors()->Proffer->config(); 103 | 104 | foreach ($config as $field => $settings) { 105 | $records = $this->{$this->Table->alias()}->find() 106 | ->select([$this->Table->primaryKey(), $field, $settings['dir']]) 107 | ->where([ 108 | "$field IS NOT NULL", 109 | "$field != ''" 110 | ]); 111 | 112 | foreach ($records as $item) { 113 | if ($this->param('verbose')) { 114 | $this->out( 115 | __('Processing ' . $this->Table->alias() . ' ' . $item->get($this->Table->primaryKey())) 116 | ); 117 | } 118 | 119 | if (!empty($this->param('path-class'))) { 120 | $class = (string)$this->param('path-class'); 121 | $path = new $class($this->Table, $item, $field, $settings); 122 | } else { 123 | $path = new ProfferPath($this->Table, $item, $field, $settings); 124 | } 125 | 126 | if (!empty($this->param('image-class'))) { 127 | $class = (string)$this->param('image-class'); 128 | $transform = new $class($this->Table, $path); 129 | } else { 130 | $transform = new ImageTransform($this->Table, $path); 131 | } 132 | 133 | $transform->processThumbnails($settings); 134 | 135 | if ($this->param('verbose')) { 136 | $this->out(__('Thumbnails regenerated for ' . $path->fullPath())); 137 | } else { 138 | $this->out(__('Thumbnails regenerated for ' . $this->Table->alias() . ' ' . $item->get($field))); 139 | } 140 | } 141 | } 142 | 143 | $this->out($this->nl(0)); 144 | $this->out(__('Completed')); 145 | } 146 | 147 | /** 148 | * Clean up files associated with a table which don't have an entry in the db 149 | * 150 | * @param string $table The name of the table 151 | * @return void 152 | */ 153 | public function cleanup($table) 154 | { 155 | $this->checkTable($table); 156 | 157 | if (!$this->param('dry-run')) { 158 | $okayToDestroy = $this->in(__('Are you sure? This will irreversibly delete files'), ['y', 'n'], 'n'); 159 | if ($okayToDestroy === 'N') { 160 | $this->out(__('Aborted, no files deleted.')); 161 | $this->_stop(); 162 | } 163 | } else { 164 | $this->out(__('Performing dry run cleanup.')); 165 | $this->out($this->nl(0)); 166 | } 167 | 168 | $config = $this->Table->behaviors()->get('Proffer')->config(); 169 | 170 | // Get the root upload folder for this table 171 | $uploadFieldFolders = glob(WWW_ROOT . 'files' . DS . strtolower($table) . DS . '*'); 172 | if (!is_array($uploadFieldFolders)) { 173 | $this->err('No files found to process.'); 174 | $this->_stop(); 175 | } 176 | 177 | // Loop through each upload field configured for this table (field) 178 | foreach ((array)$uploadFieldFolders as $fieldFolder) { 179 | // Loop through each instance of an upload for this field (seed) 180 | $pathFieldName = pathinfo((string)$fieldFolder, PATHINFO_BASENAME); 181 | $uploadFolders = glob($fieldFolder . DS . '*'); 182 | if (!is_array($uploadFolders)) { 183 | $this->err('No folders found to process.'); 184 | $this->_stop(); 185 | } 186 | 187 | foreach ((array)$uploadFolders as $seedFolder) { 188 | // Does the seed exist in the db? 189 | $seed = pathinfo((string)$seedFolder, PATHINFO_BASENAME); 190 | 191 | foreach ($config as $field => $settings) { 192 | if ($pathFieldName != $field) { 193 | continue; 194 | } 195 | 196 | $targets = []; 197 | 198 | /** @var Entity|false $record */ 199 | $record = $this->{$this->Table->getAlias()}->find() 200 | ->select([ 201 | $field, 202 | $settings['dir'] 203 | ]) 204 | ->where([ 205 | $settings['dir'] => $seed 206 | ]) 207 | ->first(); 208 | 209 | if ($record) { 210 | $record = $record->toArray(); 211 | } else { 212 | $record = []; 213 | } 214 | 215 | if (!in_array($seed, $record)) { 216 | // No it doesn't - remove the folder and it's contents - probably with a user prompt 217 | if ($this->param('dry-run')) { 218 | if ($this->param('verbose')) { 219 | $this->out(__("Would remove folder `$seedFolder`")); 220 | } else { 221 | $this->out(__("Would remove folder `$seed`")); 222 | } 223 | } else { 224 | array_map('unlink', (array)glob($seedFolder . DS . '*')); 225 | rmdir((string)$seedFolder); 226 | 227 | if ($this->param('verbose')) { 228 | $this->out(__("Remove `$seedFolder` folder and contents")); 229 | } else { 230 | $this->out(__("Removed `$seed` folder and contents")); 231 | } 232 | } 233 | } else { 234 | $files = (array)glob($seedFolder . DS . '*'); 235 | 236 | $filenames = array_map(function ($p) { 237 | return pathinfo($p, PATHINFO_BASENAME); 238 | }, $files); 239 | 240 | $targets[] = $record[$field]; 241 | if (!empty($settings['thumbnailSizes'])) { 242 | foreach ($settings['thumbnailSizes'] as $prefix => $dimensions) { 243 | $targets[] = $prefix . '_' . $record[$field]; 244 | } 245 | } 246 | 247 | $filesToRemove = array_diff($filenames, $targets); 248 | 249 | foreach ($filesToRemove as $file) { 250 | if ($this->param('dry-run') && $this->param('verbose')) { 251 | $this->out(__("Would delete `$seedFolder" . DS . "$file`")); 252 | } elseif ($this->param('dry-run')) { 253 | $this->out(__("Would delete `$file`")); 254 | } else { 255 | unlink($seedFolder . DS . $file); 256 | if ($this->param('verbose')) { 257 | $this->out(__("Deleted `$seedFolder" . DS . "$file`")); 258 | } else { 259 | $this->out(__("Deleted `$file`")); 260 | } 261 | } 262 | } 263 | } 264 | } 265 | } 266 | } 267 | 268 | $this->out($this->nl(0)); 269 | $this->out(__('Completed')); 270 | } 271 | 272 | /** 273 | * Do some checks on the table which has been passed to make sure that it has what we need 274 | * 275 | * @param string $table The table 276 | * @return void 277 | */ 278 | protected function checkTable($table) 279 | { 280 | try { 281 | $this->Table = $this->loadModel($table); 282 | } catch (Exception $e) { 283 | $this->out(__('' . $e->getMessage() . '')); 284 | $this->_stop(); 285 | } 286 | 287 | if (get_class($this->Table) === 'AppModel') { 288 | $this->out(__('The table could not be found, instance of AppModel loaded.')); 289 | $this->_stop(); 290 | } 291 | 292 | if (!$this->Table->hasBehavior('Proffer')) { 293 | $out = __( 294 | "The table '" . $this->Table->alias() . 295 | "' does not have the Proffer behavior attached." 296 | ); 297 | $this->out($out); 298 | $this->_stop(); 299 | } 300 | 301 | $config = $this->Table->behaviors()->Proffer->config(); 302 | foreach ($config as $field => $settings) { 303 | if (!$this->Table->hasField($field)) { 304 | $out = __( 305 | "The table '" . $this->Table->alias() . 306 | "' does not have the configured upload field in it's schema." 307 | ); 308 | $this->out($out); 309 | $this->_stop(); 310 | } 311 | if (!$this->Table->hasField($settings['dir'])) { 312 | $out = __( 313 | "The table '" . $this->Table->alias() . 314 | "' does not have the configured dir field in it's schema." 315 | ); 316 | $this->out($out); 317 | $this->_stop(); 318 | } 319 | } 320 | 321 | if ($this->param('remove-behaviors')) { 322 | $removeBehaviors = explode(',', (string)$this->param('remove-behaviors')); 323 | foreach ($removeBehaviors as $removeBehavior) { 324 | $this->Table->removeBehavior($removeBehavior); 325 | } 326 | } 327 | } 328 | } 329 | -------------------------------------------------------------------------------- /tests/Fixture/image_480x640.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidyell/CakePHP-Proffer/5aa63f0574da7fb2909251e626fd94e1fb32265d/tests/Fixture/image_480x640.jpg -------------------------------------------------------------------------------- /tests/Fixture/image_640x480.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidyell/CakePHP-Proffer/5aa63f0574da7fb2909251e626fd94e1fb32265d/tests/Fixture/image_640x480.jpg -------------------------------------------------------------------------------- /tests/Stubs/BadPath.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | namespace Proffer\Tests\Stubs; 10 | 11 | class BadPath 12 | { 13 | 14 | } 15 | -------------------------------------------------------------------------------- /tests/Stubs/TestPath.php: -------------------------------------------------------------------------------- 1 | 8 | * @when 02/04/15 9 | * 10 | */ 11 | 12 | namespace Proffer\Tests\Stubs; 13 | 14 | use Cake\ORM\Entity; 15 | use Cake\ORM\Table; 16 | use Proffer\Lib\ProfferPath; 17 | 18 | class TestPath extends ProfferPath 19 | { 20 | public function __construct(Table $table, Entity $entity, $field, array $settings) 21 | { 22 | $this->setRoot(TMP . 'ProfferTests'); 23 | 24 | $this->setTable($table->getAlias()); 25 | $this->setField($field); 26 | $this->setSeed('proffer_test'); 27 | 28 | if (isset($settings['thumbnailSizes'])) { 29 | $this->setPrefixes($settings['thumbnailSizes']); 30 | } 31 | 32 | $this->setFilename($entity->get($field)); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/Stubs/TestTransform.php: -------------------------------------------------------------------------------- 1 | 8 | * @when 02/04/15 9 | * 10 | */ 11 | 12 | namespace Proffer\Tests\Stubs; 13 | 14 | use Proffer\Lib\ImageTransform; 15 | 16 | class TestTransform extends ImageTransform 17 | { 18 | 19 | } 20 | -------------------------------------------------------------------------------- /tests/TestCase/Lib/ProfferPathTest.php: -------------------------------------------------------------------------------- 1 | 6 | */ 7 | namespace Proffer\Tests\Lib; 8 | 9 | use Cake\Core\Plugin; 10 | use Cake\ORM\Entity; 11 | use Cake\TestSuite\TestCase; 12 | use Proffer\Lib\ProfferPath; 13 | 14 | class ProfferPathTest extends TestCase 15 | { 16 | 17 | /** 18 | * Recursively remove files and folders 19 | * 20 | * @param $dir 21 | */ 22 | protected function _rrmdir($dir) 23 | { 24 | if (is_dir($dir)) { 25 | $objects = scandir($dir); 26 | foreach ($objects as $object) { 27 | if ($object != "." && $object != "..") { 28 | if (filetype($dir . "/" . $object) == "dir") { 29 | $this->_rrmdir($dir . "/" . $object); 30 | } else { 31 | unlink($dir . "/" . $object); 32 | } 33 | } 34 | } 35 | reset($objects); 36 | rmdir($dir); 37 | } 38 | } 39 | 40 | public function setUp() 41 | { 42 | parent::setUp(); 43 | 44 | $this->loadPlugins(['Proffer' => ['path' => ROOT]]); 45 | } 46 | 47 | /** 48 | * Clear up any generated images after each test 49 | * 50 | * @return void 51 | */ 52 | public function tearDown() 53 | { 54 | $this->_rrmdir(TMP . 'ProfferTests' . DS); 55 | } 56 | 57 | public function pathDataProvider() 58 | { 59 | return [ 60 | [ 61 | [ 62 | 'field' => 'photo', 63 | 'entity' => [ 64 | 'photo' => 'image_640x480.jpg', 65 | 'photo_dir' => 'proffer_test' 66 | ], 67 | 'settings' => [ 68 | 'photo' => [ 69 | 'root' => TMP . 'ProfferTest', 70 | 'dir' => 'photo_dir', 71 | 'thumbnailSizes' => [ 72 | 'square' => ['w' => 100, 'h' => 100], 73 | 'squareCrop' => ['w' => 100, 'h' => 100, 'crop' => true] 74 | ] 75 | ] 76 | ] 77 | ], 78 | [ 79 | TMP . 'ProfferTest' . DS . 'proffertest' . DS . 'photo' . DS . 'proffer_test' . 80 | DS . 'image_640x480.jpg', 81 | TMP . 'ProfferTest' . DS . 'proffertest' . DS . 'photo' . DS . 'proffer_test' . 82 | DS . 'square_image_640x480.jpg', 83 | TMP . 'ProfferTest' . DS . 'proffertest' . DS . 'photo' . DS . 'proffer_test' . 84 | DS . 'squareCrop_image_640x480.jpg' 85 | ] 86 | ], 87 | [ 88 | [ 89 | 'field' => 'profile_picture_image', 90 | 'entity' => [ 91 | 'profile_picture_image' => 'image_640x480.jpg', 92 | 'profile_pictures_dir' => 'proffer_test' 93 | ], 94 | 'settings' => [ 95 | 'profile_picture_image' => [ 96 | 'root' => TMP . 'ProfferTest', 97 | 'dir' => 'profile_pictures_dir', 98 | 'thumbnailSizes' => [ 99 | 'portrait' => ['w' => 300, 'h' => 100], 100 | 'portraitCropped' => ['w' => 350, 'h' => 120, 'crop' => true] 101 | ] 102 | ] 103 | ] 104 | ], 105 | [ 106 | TMP . 'ProfferTest' . DS . 'proffertest' . DS . 'profile_picture_image' . DS . 'proffer_test' . 107 | DS . 'image_640x480.jpg', 108 | TMP . 'ProfferTest' . DS . 'proffertest' . DS . 'profile_picture_image' . DS . 'proffer_test' . 109 | DS . 'portrait_image_640x480.jpg', 110 | TMP . 'ProfferTest' . DS . 'proffertest' . DS . 'profile_picture_image' . DS . 'proffer_test' . 111 | DS . 'portraitCropped_image_640x480.jpg' 112 | ] 113 | ], 114 | ]; 115 | } 116 | 117 | /** 118 | * @dataProvider pathDataProvider 119 | * @param array $data Set of data for the test 120 | * @param array $expected Expected set of results 121 | */ 122 | public function testConstructedFullPath($data, $expected) 123 | { 124 | $table = $this->getMockBuilder('Cake\ORM\Table') 125 | ->setMethods(['getAlias']) 126 | ->getMock(); 127 | $table->method('getAlias') 128 | ->willReturn('ProfferTest'); 129 | 130 | $entity = new Entity($data['entity']); 131 | 132 | $path = new ProfferPath($table, $entity, $data['field'], $data['settings'][$data['field']]); 133 | 134 | $i = 1; 135 | foreach ($data['settings'][$data['field']]['thumbnailSizes'] as $prefix => $dimensions) { 136 | $this->assertEquals($expected[$i], $path->fullPath($prefix)); 137 | $i++; 138 | } 139 | 140 | $this->assertEquals($expected[0], $path->fullPath()); 141 | } 142 | 143 | public function testGetFolder() 144 | { 145 | $table = $this->getMockBuilder('Cake\ORM\Table') 146 | ->setMethods(['getAlias']) 147 | ->getMock(); 148 | $table->method('getAlias') 149 | ->willReturn('ProfferTest'); 150 | 151 | $entity = new Entity([ 152 | 'photo' => 'image_640x480.jpg', 153 | 'photo_dir' => 'proffer_test' 154 | ]); 155 | 156 | $settings = [ 157 | 'root' => TMP . 'ProfferTest', 158 | 'dir' => 'photo_dir', 159 | 'thumbnailSizes' => [ 160 | 'square' => ['w' => 100, 'h' => 100], 161 | 'squareCrop' => ['w' => 100, 'h' => 100, 'crop' => true] 162 | ] 163 | ]; 164 | 165 | $path = new ProfferPath($table, $entity, 'photo', $settings); 166 | $result = $path->getFolder(); 167 | $expected = TMP . 'ProfferTest' . DS . 'proffertest' . DS . 'photo' . DS . 'proffer_test' . DS; 168 | 169 | $this->assertEquals($result, $expected); 170 | } 171 | 172 | public function testPrefixes() 173 | { 174 | $table = $this->getMockBuilder('Cake\ORM\Table') 175 | ->setMethods(['getAlias']) 176 | ->getMock(); 177 | $table->method('getAlias') 178 | ->willReturn('ProfferTest'); 179 | 180 | $entity = new Entity([ 181 | 'photo' => 'image_640x480.jpg', 182 | 'photo_dir' => 'proffer_test' 183 | ]); 184 | 185 | $settings = [ 186 | 'root' => TMP . 'ProfferTest', 187 | 'dir' => 'photo_dir', 188 | 'thumbnailSizes' => [ 189 | 'square' => ['w' => 100, 'h' => 100], 190 | 'squareCrop' => ['w' => 100, 'h' => 100, 'crop' => true] 191 | ] 192 | ]; 193 | $expected = ['square', 'squareCrop']; 194 | 195 | $path = new ProfferPath($table, $entity, 'photo', $settings); 196 | $result = $path->getPrefixes(); 197 | 198 | $this->assertEquals($expected, $result); 199 | } 200 | 201 | public function testDeleteFiles() 202 | { 203 | $table = $this->getMockBuilder('Cake\ORM\Table') 204 | ->setMethods(['getAlias']) 205 | ->getMock(); 206 | $table->method('getAlias') 207 | ->willReturn('ProfferTest'); 208 | 209 | $entity = new Entity([ 210 | 'photo' => 'image_640x480.jpg', 211 | 'photo_dir' => 'proffer_test' 212 | ]); 213 | 214 | $settings = [ 215 | 'root' => TMP . 'ProfferTests', 216 | 'dir' => 'photo_dir', 217 | 'thumbnailSizes' => [ 218 | 'square' => ['w' => 100, 'h' => 100], 219 | 'squareCrop' => ['w' => 100, 'h' => 100, 'crop' => true] 220 | ] 221 | ]; 222 | 223 | $path = $this->getMockBuilder('Proffer\Lib\ProfferPath') 224 | ->setConstructorArgs([$table, $entity, 'photo', $settings]) 225 | ->setMethods(['getFolder']) 226 | ->getMock(); 227 | 228 | $path->expects($this->any()) 229 | ->method('getFolder') 230 | ->willReturn(TMP . 'ProfferTests' . DS . $table->getAlias() . DS . 'photo' . DS . 'proffer_test' . DS); 231 | 232 | $path = new ProfferPath($table, $entity, 'photo', $settings); 233 | 234 | if (!file_exists($path->getFolder())) { 235 | mkdir($path->getFolder(), 0777, true); 236 | } 237 | 238 | copy( 239 | Plugin::path('Proffer') . 'tests' . DS . 'Fixture' . 240 | DS . 'image_640x480.jpg', 241 | $path->getFolder() . 'image_640x480.jpg' 242 | ); 243 | copy( 244 | Plugin::path('Proffer') . 'tests' . DS . 'Fixture' . 245 | DS . 'image_640x480.jpg', 246 | $path->getFolder() . 'square_image_640x480.jpg' 247 | ); 248 | copy( 249 | Plugin::path('Proffer') . 'tests' . DS . 'Fixture' . 250 | DS . 'image_640x480.jpg', 251 | $path->getFolder() . 'portrait_image_640x480.jpg' 252 | ); 253 | 254 | $path->deleteFiles($path->getFolder()); 255 | 256 | $this->assertFileNotExists($path->getFolder() . 'image_640x480.jpg'); 257 | $this->assertFileNotExists($path->getFolder() . 'square_image_640x480.jpg'); 258 | $this->assertFileNotExists($path->getFolder() . 'portrait_image_640x480.jpg'); 259 | 260 | $path->deleteFiles($path->getFolder(), true); 261 | 262 | $this->assertFileNotExists($path->getFolder()); 263 | } 264 | 265 | public function testCreatingPathFolderWhichExists() 266 | { 267 | $table = $this->getMockBuilder('Cake\ORM\Table') 268 | ->setMethods(['getAlias']) 269 | ->getMock(); 270 | $table->method('getAlias') 271 | ->willReturn('ProfferTest'); 272 | 273 | $entity = new Entity([ 274 | 'photo' => 'image_640x480.jpg', 275 | 'photo_dir' => 'proffer_test' 276 | ]); 277 | 278 | $settings = [ 279 | 'root' => TMP . 'ProfferTests', 280 | 'dir' => 'photo_dir', 281 | 'thumbnailSizes' => [ 282 | 'square' => ['w' => 100, 'h' => 100], 283 | 'squareCrop' => ['w' => 100, 'h' => 100, 'crop' => true] 284 | ] 285 | ]; 286 | 287 | $path = new ProfferPath($table, $entity, 'photo', $settings); 288 | 289 | mkdir(TMP . 'ProfferTests' . DS . 'proffertest' . DS . 'photo' . DS . 'proffer_test' . DS, 0777, true); 290 | 291 | $result = $path->createPathFolder(); 292 | $this->assertEquals(true, $result); 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /tests/TestCase/Model/Behavior/ProfferBehaviorTest.php: -------------------------------------------------------------------------------- 1 | 6 | */ 7 | 8 | namespace Proffer\Tests\Model\Behavior; 9 | 10 | use ArrayObject; 11 | use Cake\Core\Plugin; 12 | use Cake\Database\Schema\TableSchema; 13 | use Cake\Event\Event; 14 | use Cake\Event\EventListenerInterface; 15 | use Cake\Event\EventManager; 16 | use Cake\ORM\Entity; 17 | use Cake\ORM\Table; 18 | use Cake\TestSuite\TestCase; 19 | use Cake\Validation\Validator; 20 | use Proffer\Lib\ProfferPath; 21 | use Proffer\Model\Behavior\ProfferBehavior; 22 | use Proffer\Tests\Stubs\TestPath; 23 | 24 | /** 25 | * Class ProfferBehaviorTest 26 | * 27 | * @package Proffer\Tests\Model\Behavior 28 | */ 29 | class ProfferBehaviorTest extends TestCase 30 | { 31 | 32 | private $config = [ 33 | 'photo' => [ 34 | 'dir' => 'photo_dir', 35 | 'thumbnailSizes' => [ 36 | 'square' => ['w' => 200, 'h' => 200, 'crop' => true], 37 | 'portrait' => ['w' => 100, 'h' => 300], 38 | 'large' => ['w' => 1200, 'h' => 900, 'orientate' => true], 39 | ] 40 | ] 41 | ]; 42 | 43 | /** 44 | * Adjust the default root so that it doesn't overwrite and user files 45 | */ 46 | public function setUp() 47 | { 48 | $this->loadPlugins([ 49 | 'Proffer' => ['path' => ROOT] 50 | ]); 51 | 52 | $this->config['photo']['root'] = TMP . 'ProfferTests' . DS; 53 | } 54 | 55 | /** 56 | * Recursively remove files and folders 57 | * 58 | * @param $dir 59 | */ 60 | protected function _rrmdir($dir) 61 | { 62 | if (is_dir($dir)) { 63 | $objects = scandir($dir); 64 | foreach ($objects as $object) { 65 | if ($object != "." && $object != "..") { 66 | if (filetype($dir . "/" . $object) == "dir") { 67 | $this->_rrmdir($dir . "/" . $object); 68 | } else { 69 | unlink($dir . "/" . $object); 70 | } 71 | } 72 | } 73 | reset($objects); 74 | rmdir($dir); 75 | } 76 | } 77 | 78 | /** 79 | * Clear up any generated images after each test 80 | * 81 | * @return void 82 | */ 83 | public function tearDown() 84 | { 85 | $this->_rrmdir(TMP . 'ProfferTests' . DS); 86 | } 87 | 88 | /** 89 | * Generate a mock of the ProfferPath class with various set returns to ensure that the path is always consistent 90 | * 91 | * @param Table $table Instance of the table 92 | * @param Entity $entity Instance of the entity 93 | * @return \PHPUnit_Framework_MockObject_MockObject 94 | */ 95 | protected function _getProfferPathMock(Table $table, Entity $entity) 96 | { 97 | $path = $this->getMockBuilder(ProfferPath::class) 98 | ->setConstructorArgs([$table, $entity, 'photo', $this->config['photo']]) 99 | ->setMethods(['fullPath', 'getFolder']) 100 | ->getMock(); 101 | 102 | $path->expects($this->any()) 103 | ->method('fullPath') 104 | ->with($this->logicalOr( 105 | $this->equalTo(null), 106 | $this->equalTo('square'), 107 | $this->equalTo('portrait'), 108 | $this->equalTo('large') 109 | )) 110 | ->will($this->returnCallback( 111 | function ($param) use ($table, $entity) { 112 | $filename = ''; 113 | if ($param !== null) { 114 | $filename = $param . '_'; 115 | } 116 | 117 | $entityFieldData = $entity->get('photo'); 118 | 119 | if (is_array($entityFieldData)) { 120 | $filename .= $entityFieldData['name']; 121 | } else { 122 | $filename .= $entityFieldData; 123 | } 124 | 125 | return TMP . 'ProfferTests' . DS . $table->getAlias() . 126 | DS . 'photo' . DS . 'proffer_test' . DS . $filename; 127 | } 128 | )); 129 | 130 | $path->expects($this->any()) 131 | ->method('getFolder') 132 | ->willReturn(TMP . 'ProfferTests' . DS . $table->getAlias() . DS . 'photo' . DS . 'proffer_test' . DS); 133 | 134 | return $path; 135 | } 136 | 137 | /** 138 | * Data provider method for testing validation 139 | * 140 | * @return array 141 | */ 142 | public function beforeMarshalProvider() 143 | { 144 | return [ 145 | [ 146 | ['photo' => ['error' => UPLOAD_ERR_NO_FILE]], 147 | true, 148 | ['photo' => ['error' => UPLOAD_ERR_NO_FILE]] 149 | ], 150 | [ 151 | ['photo' => ['error' => UPLOAD_ERR_NO_FILE]], 152 | false, 153 | ['photo' => ['error' => UPLOAD_ERR_NO_FILE]] 154 | ], 155 | [ 156 | ['photo' => ['error' => UPLOAD_ERR_OK]], 157 | true, 158 | ['photo' => ['error' => UPLOAD_ERR_OK]] 159 | ], 160 | [ 161 | ['photo' => ['error' => UPLOAD_ERR_OK]], 162 | false, 163 | ['photo' => ['error' => UPLOAD_ERR_OK]] 164 | ], 165 | ]; 166 | } 167 | 168 | /** 169 | * @dataProvider beforeMarshalProvider 170 | * 171 | * @param array $data 172 | * @param bool $allowEmpty 173 | * @param array $expected 174 | */ 175 | public function testBeforeMarshal(array $data, $allowEmpty, array $expected) 176 | { 177 | $schema = $this->createMock(TableSchema::class); 178 | $table = $this->createMock(Table::class); 179 | $table->method('getAlias') 180 | ->willReturn('ProfferTest'); 181 | $table->method('getSchema') 182 | ->willReturn($schema); 183 | 184 | $proffer = new ProfferBehavior($table, $this->config); 185 | 186 | $validator = $this->createMock(Validator::class); 187 | $table->setValidator('test', $validator); 188 | 189 | $table->method('getValidator') 190 | ->willReturn($validator); 191 | 192 | if ($allowEmpty) { 193 | $table->getValidator()->allowEmpty('photo'); 194 | } 195 | 196 | $arrayObject = new ArrayObject($data); 197 | 198 | $proffer->beforeMarshal( 199 | $this->createMock(Event::class), 200 | $arrayObject, 201 | new ArrayObject() 202 | ); 203 | $result = $arrayObject; 204 | 205 | $this->assertEquals(new ArrayObject($expected), $result); 206 | } 207 | 208 | /** 209 | * Data provider method for testing valid file uploads 210 | * 211 | * @return array 212 | */ 213 | public function validFileProvider() 214 | { 215 | return [ 216 | 'landscape image' => [ 217 | [ 218 | 'photo' => [ 219 | 'name' => 'image_640x480.jpg', 220 | 'tmp_name' => ROOT . 'tests' . DS . 'Fixture' . DS . 'image_640x480.jpg', 221 | 'size' => 33000, 222 | 'error' => UPLOAD_ERR_OK 223 | ], 224 | 'photo_dir' => 'proffer_test' 225 | ], 226 | [ 227 | 'filename' => 'image_640x480.jpg', 228 | 'dir' => 'proffer_test' 229 | ] 230 | ], 231 | 'portrait image' => [ 232 | [ 233 | 'photo' => [ 234 | 'name' => 'image_480x640.jpg', 235 | 'tmp_name' => ROOT . 'tests' . DS . 'Fixture' . DS . 'image_480x640.jpg', 236 | 'size' => 45704, 237 | 'error' => UPLOAD_ERR_OK 238 | ], 239 | 'photo_dir' => 'proffer_test' 240 | ], 241 | [ 242 | 'filename' => 'image_480x640.jpg', 243 | 'dir' => 'proffer_test' 244 | ] 245 | ] 246 | ]; 247 | } 248 | 249 | /** 250 | * A bit of a unit and integration test as it will still dispatch the events to the listener 251 | * 252 | * @dataProvider validFileProvider 253 | * 254 | * @param array $entityData 255 | * @param array $expected 256 | */ 257 | public function testBeforeSaveWithValidFile(array $entityData, array $expected) 258 | { 259 | $schema = $this->createMock(TableSchema::class); 260 | $table = $this->createMock(Table::class); 261 | $eventManager = $this->createMock(EventManager::class); 262 | $table->method('getAlias') 263 | ->willReturn('ProfferTest'); 264 | $table->method('getSchema') 265 | ->willReturn($schema); 266 | $table->method('getEventManager') 267 | ->willReturn($eventManager); 268 | 269 | $entity = new Entity($entityData); 270 | $path = $this->_getProfferPathMock($table, $entity, 'photo'); 271 | 272 | $proffer = $this->getMockBuilder(ProfferBehavior::class) 273 | ->setConstructorArgs([$table, $this->config]) 274 | ->setMethods(['moveUploadedFile']) 275 | ->getMock(); 276 | 277 | $proffer->expects($this->once()) 278 | ->method('moveUploadedFile') 279 | ->willReturnCallback(function ($source, $destination) { 280 | if (!file_exists(pathinfo($destination, PATHINFO_DIRNAME))) { 281 | mkdir(pathinfo($destination, PATHINFO_DIRNAME), 0777, true); 282 | } 283 | 284 | return copy($source, $destination); 285 | }); 286 | 287 | $proffer->beforeSave( 288 | $this->createMock(Event::class), 289 | $entity, 290 | new ArrayObject(), 291 | $path 292 | ); 293 | 294 | $this->assertEquals($expected['filename'], $entity->get('photo')); 295 | $this->assertEquals($expected['dir'], $entity->get('photo_dir')); 296 | 297 | $testUploadPath = $path->getFolder(); 298 | 299 | $this->assertFileExists($testUploadPath . $expected['filename']); 300 | $this->assertFileExists($testUploadPath . 'portrait_' . $expected['filename']); 301 | $this->assertFileExists($testUploadPath . 'square_' . $expected['filename']); 302 | 303 | $portraitSizes = getimagesize($testUploadPath . 'portrait_' . $expected['filename']); 304 | $this->assertEquals(100, $portraitSizes[0]); 305 | 306 | $squareSizes = getimagesize($testUploadPath . 'square_' . $expected['filename']); 307 | $this->assertEquals(200, $squareSizes[0]); 308 | $this->assertEquals(200, $squareSizes[1]); 309 | } 310 | 311 | /** 312 | * @expectedException \Proffer\Exception\CannotUploadFileException 313 | */ 314 | public function testBeforeSaveWithoutUploadingAFile() 315 | { 316 | $schema = $this->createMock(TableSchema::class); 317 | $table = $this->createMock(Table::class); 318 | $eventManager = $this->createMock(EventManager::class); 319 | $table->method('getAlias') 320 | ->willReturn('ProfferTest'); 321 | $table->method('getSchema') 322 | ->willReturn($schema); 323 | $table->method('getEventManager') 324 | ->willReturn($eventManager); 325 | 326 | $path = $this->_getProfferPathMock( 327 | $table, 328 | new Entity(['photo' => 'image_640x480.jpg', 'photo_dir' => 'proffer_test']), 329 | 'photo' 330 | ); 331 | 332 | $proffer = $this->getMockBuilder(ProfferBehavior::class) 333 | ->setConstructorArgs([$table, $this->config]) 334 | ->setMethods(['moveUploadedFile']) 335 | ->getMock(); 336 | 337 | $proffer->expects($this->once()) 338 | ->method('moveUploadedFile') 339 | ->willReturn(false); 340 | 341 | $entity = new Entity([ 342 | 'photo' => [ 343 | 'name' => '', 344 | 'tmp_name' => '', 345 | 'size' => '', 346 | 'error' => UPLOAD_ERR_OK 347 | ] 348 | ]); 349 | 350 | $proffer->beforeSave( 351 | $this->createMock(Event::class), 352 | $entity, 353 | new ArrayObject(), 354 | $path 355 | ); 356 | } 357 | 358 | /** 359 | * @expectedException \Proffer\Exception\CannotUploadFileException 360 | */ 361 | public function testFailedToMoveFile() 362 | { 363 | $schema = $this->createMock(TableSchema::class); 364 | $table = $this->createMock(Table::class); 365 | $eventManager = $this->createMock(EventManager::class); 366 | $table->method('getAlias') 367 | ->willReturn('ProfferTest'); 368 | $table->method('getSchema') 369 | ->willReturn($schema); 370 | $table->method('getEventManager') 371 | ->willReturn($eventManager); 372 | 373 | $proffer = $this->getMockBuilder(ProfferBehavior::class) 374 | ->setConstructorArgs([$table, $this->config]) 375 | ->setMethods(['moveUploadedFile']) 376 | ->getMock(); 377 | 378 | $proffer->expects($this->once()) 379 | ->method('moveUploadedFile') 380 | ->willReturn(false); 381 | 382 | $entity = new Entity([ 383 | 'photo' => [ 384 | 'name' => 'image_640x480.jpg', 385 | 'tmp_name' => Plugin::path('Proffer') . 'tests' . DS . 'Fixture' . DS . 'image_640x480.jpg', 386 | 'size' => 33000, 387 | 'error' => UPLOAD_ERR_OK 388 | ] 389 | ]); 390 | 391 | $path = $this->_getProfferPathMock($table, $entity, 'photo'); 392 | 393 | $proffer->beforeSave( 394 | $this->createMock(Event::class), 395 | $entity, 396 | new ArrayObject(), 397 | $path 398 | ); 399 | } 400 | 401 | /** 402 | * Test afterDelete 403 | */ 404 | public function testAfterDelete() 405 | { 406 | $schema = $this->createMock(TableSchema::class); 407 | $eventManager = $this->createMock(EventManager::class); 408 | $table = $this->createMock(Table::class); 409 | $table->method('getAlias') 410 | ->willReturn('ProfferTest'); 411 | $table->method('getSchema') 412 | ->willReturn($schema); 413 | $table->method('getEventManager') 414 | ->willReturn($eventManager); 415 | 416 | $proffer = new ProfferBehavior($table, $this->config); 417 | 418 | $entity = new Entity([ 419 | 'photo' => 'image_640x480.jpg', 420 | 'photo_dir' => 'proffer_test' 421 | ]); 422 | 423 | $path = $this->_getProfferPathMock($table, $entity, 'photo'); 424 | $testUploadPath = $path->getFolder(); 425 | 426 | if (!file_exists($testUploadPath)) { 427 | mkdir($testUploadPath, 0777, true); 428 | } 429 | 430 | copy( 431 | Plugin::path('Proffer') . 'tests' . DS . 'Fixture' . DS . 'image_640x480.jpg', 432 | $testUploadPath . 'image_640x480.jpg' 433 | ); 434 | copy( 435 | Plugin::path('Proffer') . 'tests' . DS . 'Fixture' . DS . 'image_640x480.jpg', 436 | $testUploadPath . 'square_image_640x480.jpg' 437 | ); 438 | copy( 439 | Plugin::path('Proffer') . 'tests' . DS . 'Fixture' . DS . 'image_640x480.jpg', 440 | $testUploadPath . 'portrait_image_640x480.jpg' 441 | ); 442 | 443 | $event = new Event('Proffer.beforeDeleteFolder', $entity, ['path' => $path]); 444 | $eventManager->expects($this->at(0)) 445 | ->method('dispatch') 446 | ->with($this->equalTo($event)); 447 | 448 | $proffer->afterDelete( 449 | $this->createMock(Event::class), 450 | $entity, 451 | new ArrayObject(), 452 | $path 453 | ); 454 | 455 | $this->assertFileNotExists($testUploadPath . 'image_640x480.jpg'); 456 | $this->assertFileNotExists($testUploadPath . 'square_image_640x480.jpg'); 457 | $this->assertFileNotExists($testUploadPath . 'portrait_image_640x480.jpg'); 458 | } 459 | 460 | public function testAfterDeleteWithMissingFiles() 461 | { 462 | $schema = $this->createMock(TableSchema::class); 463 | $table = $this->createMock(Table::class); 464 | $eventManager = $this->createMock(EventManager::class); 465 | $table->method('getAlias') 466 | ->willReturn('ProfferTest'); 467 | $table->method('getSchema') 468 | ->willReturn($schema); 469 | $table->method('getEventManager') 470 | ->willReturn($eventManager); 471 | 472 | $proffer = new ProfferBehavior($table, $this->config); 473 | 474 | $entity = new Entity([ 475 | 'photo' => 'image_640x480.jpg', 476 | 'photo_dir' => 'proffer_test' 477 | ]); 478 | 479 | $path = $this->_getProfferPathMock($table, $entity, 'photo'); 480 | $testUploadPath = $path->getFolder(); 481 | 482 | if (!file_exists($testUploadPath)) { 483 | mkdir($testUploadPath, 0777, true); 484 | } 485 | 486 | copy( 487 | Plugin::path('Proffer') . 'tests' . DS . 'Fixture' . DS . 'image_640x480.jpg', 488 | $testUploadPath . 'image_640x480.jpg' 489 | ); 490 | 491 | $proffer->afterDelete( 492 | $this->createMock(Event::class), 493 | $entity, 494 | new ArrayObject(), 495 | $path 496 | ); 497 | 498 | $this->assertFileNotExists($testUploadPath . 'image_640x480.jpg'); 499 | $this->assertFileNotExists($testUploadPath . 'square_image_640x480.jpg'); 500 | $this->assertFileNotExists($testUploadPath . 'portrait_image_640x480.jpg'); 501 | } 502 | 503 | public function testEventsForBeforeSave() 504 | { 505 | $entityData = [ 506 | 'photo' => [ 507 | 'name' => 'image_640x480.jpg', 508 | 'tmp_name' => Plugin::path('Proffer') . 'tests' . DS . 'Fixture' . DS . 'image_640x480.jpg', 509 | 'size' => 33000, 510 | 'error' => UPLOAD_ERR_OK 511 | ], 512 | 'photo_dir' => 'proffer_test' 513 | ]; 514 | $entity = new Entity($entityData); 515 | 516 | $eventManager = $this->createMock(EventManager::class); 517 | 518 | $schema = $this->createMock(TableSchema::class); 519 | $table = $this->getMockBuilder(Table::class) 520 | ->setConstructorArgs([['eventManager' => $eventManager, 'schema' => $schema]]) 521 | ->setMethods(['getAlias']) 522 | ->getMock(); 523 | 524 | $table->method('getAlias') 525 | ->willReturn('ProfferTest'); 526 | 527 | $path = $this->_getProfferPathMock($table, $entity, 'photo'); 528 | 529 | $eventAfterPath = new Event('Proffer.afterPath', $entity, ['path' => $path]); 530 | 531 | $eventManager->expects($this->at(0)) 532 | ->method('dispatch') 533 | ->with($this->equalTo($eventAfterPath)); 534 | 535 | $images = [ 536 | $path->getFolder() . 'image_640x480.jpg', 537 | $path->getFolder() . 'square_image_640x480.jpg', 538 | $path->getFolder() . 'portrait_image_640x480.jpg', 539 | $path->getFolder() . 'large_image_640x480.jpg', 540 | ]; 541 | $eventAfterCreateImage = new Event('Proffer.afterCreateImage', $entity, ['path' => $path, 'images' => $images]); 542 | 543 | $eventManager->expects($this->at(1)) 544 | ->method('dispatch') 545 | ->with($this->equalTo($eventAfterCreateImage)); 546 | 547 | $proffer = $this->getMockBuilder(ProfferBehavior::class) 548 | ->setConstructorArgs([$table, $this->config]) 549 | ->setMethods(['moveUploadedFile']) 550 | ->getMock(); 551 | 552 | $proffer->expects($this->once()) 553 | ->method('moveUploadedFile') 554 | ->will($this->returnCallback( 555 | function ($param) use ($entity, $path) { 556 | return copy($entity->get('photo')['tmp_name'], $path->fullPath()); 557 | } 558 | )); 559 | 560 | $proffer->beforeSave( 561 | $this->createMock(Event::class), 562 | $entity, 563 | new ArrayObject(), 564 | $path 565 | ); 566 | } 567 | 568 | public function testThumbsNotCreatedWhenNoSizes() 569 | { 570 | $schema = $this->createMock(TableSchema::class); 571 | $table = $this->createMock(Table::class); 572 | $eventManager = $this->createMock(EventManager::class); 573 | $table->method('getAlias') 574 | ->willReturn('ProfferTest'); 575 | $table->method('getSchema') 576 | ->willReturn($schema); 577 | $table->method('getEventManager') 578 | ->willReturn($eventManager); 579 | 580 | $config = $this->config; 581 | unset($config['photo']['thumbnailSizes']); 582 | 583 | $entityData = [ 584 | 'photo' => [ 585 | 'name' => 'image_640x480.jpg', 586 | 'tmp_name' => Plugin::path('Proffer') . 'tests' . DS . 'Fixture' . DS . 'image_640x480.jpg', 587 | 'size' => 33000, 588 | 'error' => UPLOAD_ERR_OK 589 | ], 590 | 'photo_dir' => 'proffer_test' 591 | ]; 592 | $entity = new Entity($entityData); 593 | $path = $this->_getProfferPathMock($table, $entity, 'photo'); 594 | 595 | $proffer = $this->getMockBuilder(ProfferBehavior::class) 596 | ->setConstructorArgs([$table, $config]) 597 | ->setMethods(['moveUploadedFile']) 598 | ->getMock(); 599 | 600 | $proffer->expects($this->once()) 601 | ->method('moveUploadedFile') 602 | ->willReturnCallback(function ($source, $destination) { 603 | if (!file_exists(pathinfo($destination, PATHINFO_DIRNAME))) { 604 | mkdir(pathinfo($destination, PATHINFO_DIRNAME), 0777, true); 605 | } 606 | 607 | return copy($source, $destination); 608 | }); 609 | 610 | $proffer->beforeSave( 611 | $this->createMock(Event::class), 612 | $entity, 613 | new ArrayObject(), 614 | $path 615 | ); 616 | 617 | $this->assertEquals('image_640x480.jpg', $entity->get('photo')); 618 | $this->assertEquals('proffer_test', $entity->get('photo_dir')); 619 | 620 | $testUploadPath = $path->getFolder(); 621 | 622 | $this->assertFileExists($testUploadPath . 'image_640x480.jpg'); 623 | $this->assertFileNotExists($testUploadPath . 'portrait_image_640x480.jpg'); 624 | $this->assertFileNotExists($testUploadPath . 'square_image_640x480.jpg'); 625 | } 626 | 627 | public function providerPathEvents() 628 | { 629 | return [ 630 | [ 631 | [ 632 | 'table' => 'proffer_path_event_test', 633 | 'seed' => 'proffer_event_test', 634 | 'filename' => 'event_image_640x480.jpg' 635 | ], 636 | TMP . 'ProfferTests' . DS . 'proffer_path_event_test' . DS . 'photo' . DS . 'proffer_event_test' . 637 | DS . 'event_image_640x480.jpg' 638 | ], 639 | [ 640 | [ 641 | 'table' => null, 642 | 'seed' => 'proffer_event_test', 643 | 'filename' => 'event_image_640x480.jpg' 644 | ], 645 | TMP . 'ProfferTests' . DS . 'photo' . DS . 'proffer_event_test' . DS . 'event_image_640x480.jpg' 646 | ], 647 | [ 648 | [ 649 | 'table' => '', 650 | 'seed' => 'proffer_event_test', 651 | 'filename' => 'event_image_640x480.jpg' 652 | ], 653 | TMP . 'ProfferTests' . DS . 'photo' . DS . 'proffer_event_test' . DS . 'event_image_640x480.jpg' 654 | ], 655 | [ 656 | [ 657 | 'table' => '', 658 | 'seed' => '', 659 | 'filename' => 'event_image_640x480.jpg' 660 | ], 661 | TMP . 'ProfferTests' . DS . 'photo' . DS . 'event_image_640x480.jpg' 662 | ], 663 | [ 664 | [ 665 | 'table' => 'proffer_path_event_test', 666 | 'seed' => '', 667 | 'filename' => 'event_image_640x480.jpg' 668 | ], 669 | TMP . 'ProfferTests' . DS . 'proffer_path_event_test' . DS . 'photo' . DS . 'event_image_640x480.jpg' 670 | ], 671 | ]; 672 | } 673 | 674 | /** 675 | * @param array $pathData An array of data to pass into the path customisation 676 | * @param string $expected 677 | * 678 | * @dataProvider providerPathEvents 679 | */ 680 | public function testChangingThePathUsingEvents(array $pathData, $expected) 681 | { 682 | $schema = $this->createMock(TableSchema::class); 683 | $table = $this->createMock(Table::class); 684 | $eventManager = new EventManager(); 685 | $table->method('getAlias') 686 | ->willReturn('ProfferTest'); 687 | $table->method('getSchema') 688 | ->willReturn($schema); 689 | $table->method('getEventManager') 690 | ->willReturn($eventManager); 691 | 692 | $listener = $this->getMockBuilder(EventListenerInterface::class) 693 | ->setMethods(['implementedEvents', 'filename']) 694 | ->getMock(); 695 | 696 | $listener->expects($this->once()) 697 | ->method('implementedEvents') 698 | ->willReturn(['Proffer.afterPath' => 'filename']); 699 | 700 | $listener->expects($this->once()) 701 | ->method('filename') 702 | ->willReturnCallback(function ($event, $path) use ($pathData) { 703 | $path->setTable($pathData['table']); 704 | $path->setSeed($pathData['seed']); 705 | $path->setFilename($pathData['filename']); 706 | 707 | $event->getSubject()['photo']['name'] = $pathData['filename']; 708 | 709 | return $path; 710 | }); 711 | 712 | $table->getEventManager()->on($listener); 713 | 714 | $entityData = [ 715 | 'photo' => [ 716 | 'name' => 'image_640x480.jpg', 717 | 'tmp_name' => Plugin::path('Proffer') . 'tests' . DS . 'Fixture' . DS . 'image_640x480.jpg', 718 | 'size' => 33000, 719 | 'error' => UPLOAD_ERR_OK 720 | ], 721 | 'photo_dir' => 'proffer_test' 722 | ]; 723 | $entity = new Entity($entityData); 724 | 725 | $this->config['photo']['root'] = TMP . 'ProfferTests'; 726 | $path = new ProfferPath($table, $entity, 'photo', $this->config['photo']); 727 | 728 | $proffer = $this->getMockBuilder(ProfferBehavior::class) 729 | ->setConstructorArgs([$table, $this->config]) 730 | ->setMethods(['moveUploadedFile']) 731 | ->getMock(); 732 | 733 | $proffer->expects($this->once()) 734 | ->method('moveUploadedFile') 735 | ->willReturnCallback(function ($source, $destination) { 736 | if (!file_exists(pathinfo($destination, PATHINFO_DIRNAME))) { 737 | mkdir(pathinfo($destination, PATHINFO_DIRNAME), 0777, true); 738 | } 739 | 740 | return copy($source, $destination); 741 | }); 742 | 743 | $proffer->beforeSave( 744 | $this->createMock(Event::class), 745 | $entity, 746 | new ArrayObject(), 747 | $path 748 | ); 749 | 750 | $this->assertEquals($pathData['filename'], $entity->get('photo')); 751 | $this->assertEquals($pathData['seed'], $entity->get('photo_dir')); 752 | 753 | $this->assertFileExists($path->fullPath()); 754 | $this->assertEquals($expected, $path->fullPath()); 755 | } 756 | 757 | public function testDeletingARecordWithNoThumbnailConfig() 758 | { 759 | $schema = $this->createMock(TableSchema::class); 760 | $table = $this->createMock(Table::class); 761 | $table->method('getAlias') 762 | ->willReturn('ProfferTest'); 763 | $table->method('getSchema') 764 | ->willReturn($schema); 765 | 766 | $config = $this->config; 767 | unset($config['photo']['thumbnailSizes']); 768 | 769 | $entityData = [ 770 | 'photo' => 'image_640x480.jpg', 771 | 'photo_dir' => 'proffer_test' 772 | ]; 773 | $entity = new Entity($entityData); 774 | $path = $this->_getProfferPathMock($table, $entity, 'photo'); 775 | 776 | $proffer = $this->getMockBuilder(ProfferBehavior::class) 777 | ->setConstructorArgs([$table, $config]) 778 | ->setMethods(['afterDelete']) 779 | ->getMock(); 780 | 781 | $proffer->expects($this->once()) 782 | ->method('afterDelete'); 783 | 784 | $proffer->afterDelete( 785 | $this->createMock(Event::class), 786 | $entity, 787 | new ArrayObject(), 788 | $path 789 | ); 790 | } 791 | 792 | public function testReplacingComponents() 793 | { 794 | $schema = $this->createMock(TableSchema::class); 795 | $table = $this->createMock(Table::class); 796 | $eventManager = $this->createMock(EventManager::class); 797 | $table->method('getAlias') 798 | ->willReturn('ProfferTest'); 799 | $table->method('getSchema') 800 | ->willReturn($schema); 801 | $table->method('getEventManager') 802 | ->willReturn($eventManager); 803 | 804 | $config = [ 805 | 'photo' => [ 806 | 'dir' => 'photo_dir', 807 | 'thumbnailSizes' => [ 808 | 'square' => ['w' => 200, 'h' => 200, 'crop' => true], 809 | 'portrait' => ['w' => 100, 'h' => 300], 810 | ], 811 | 'pathClass' => '\Proffer\Tests\Stubs\TestPath', 812 | 'transformClass' => '\Proffer\Tests\Stubs\TestTransform' 813 | ] 814 | ]; 815 | 816 | $entityData = [ 817 | 'photo' => [ 818 | 'name' => 'image_640x480.jpg', 819 | 'tmp_name' => Plugin::path('Proffer') . 'tests' . DS . 'Fixture' . DS . 'image_640x480.jpg', 820 | 'size' => 33000, 821 | 'error' => UPLOAD_ERR_OK 822 | ], 823 | 'photo_dir' => 'proffer_test' 824 | ]; 825 | $entity = new Entity($entityData); 826 | 827 | $proffer = $this->getMockBuilder(ProfferBehavior::class) 828 | ->setConstructorArgs([$table, $config]) 829 | ->setMethods(['moveUploadedFile']) 830 | ->getMock(); 831 | 832 | $path = new TestPath($table, $entity, 'photo', $config['photo']); 833 | 834 | $proffer->expects($this->once()) 835 | ->method('moveUploadedFile') 836 | ->willReturnCallback(function ($source, $destination) { 837 | if (!file_exists(pathinfo($destination, PATHINFO_DIRNAME))) { 838 | mkdir(pathinfo($destination, PATHINFO_DIRNAME), 0777, true); 839 | } 840 | 841 | return copy($source, $destination); 842 | }); 843 | 844 | $proffer->beforeSave( 845 | $this->createMock('Cake\Event\Event', null, ['beforeSave']), 846 | $entity, 847 | new ArrayObject() 848 | ); 849 | 850 | $this->assertEquals('image_640x480.jpg', $entity->get('photo')); 851 | $this->assertEquals('proffer_test', $entity->get('photo_dir')); 852 | 853 | $testUploadPath = $path->getFolder(); 854 | 855 | $this->assertFileExists($testUploadPath . 'image_640x480.jpg'); 856 | $this->assertFileExists($testUploadPath . 'portrait_' . 'image_640x480.jpg'); 857 | $this->assertFileExists($testUploadPath . 'square_' . 'image_640x480.jpg'); 858 | 859 | $portraitSizes = getimagesize($testUploadPath . 'portrait_' . 'image_640x480.jpg'); 860 | $this->assertEquals(100, $portraitSizes[0]); 861 | 862 | $squareSizes = getimagesize($testUploadPath . 'square_' . 'image_640x480.jpg'); 863 | $this->assertEquals(200, $squareSizes[0]); 864 | $this->assertEquals(200, $squareSizes[1]); 865 | } 866 | 867 | /** 868 | * @expectedException \Proffer\Exception\InvalidClassException 869 | */ 870 | public function testReplacingComponentsWithNoInterface() 871 | { 872 | $schema = $this->createMock(TableSchema::class); 873 | $table = $this->createMock(Table::class); 874 | $eventManager = $this->createMock(EventManager::class); 875 | $table->method('getAlias') 876 | ->willReturn('ProfferTest'); 877 | $table->method('getSchema') 878 | ->willReturn($schema); 879 | $table->method('getEventManager') 880 | ->willReturn($eventManager); 881 | 882 | $config = [ 883 | 'photo' => [ 884 | 'dir' => 'photo_dir', 885 | 'thumbnailSizes' => [ 886 | 'square' => ['w' => 200, 'h' => 200, 'crop' => true], 887 | 'portrait' => ['w' => 100, 'h' => 300], 888 | ], 889 | 'pathClass' => \Proffer\Tests\Stubs\BadPath::class, 890 | ] 891 | ]; 892 | 893 | $entityData = [ 894 | 'photo' => [ 895 | 'name' => 'image_640x480.jpg', 896 | 'tmp_name' => Plugin::path('Proffer') . 'tests' . DS . 'Fixture' . DS . 'image_640x480.jpg', 897 | 'size' => 33000, 898 | 'error' => UPLOAD_ERR_OK 899 | ], 900 | 'photo_dir' => 'proffer_test' 901 | ]; 902 | $entity = new Entity($entityData); 903 | 904 | $proffer = $this->getMockBuilder(ProfferBehavior::class) 905 | ->setConstructorArgs([$table, $config]) 906 | ->setMethods(['moveUploadedFile']) 907 | ->getMock(); 908 | 909 | $path = new TestPath($table, $entity, 'photo', $config['photo']); 910 | 911 | $proffer->expects($this->never()) 912 | ->method('moveUploadedFile'); 913 | 914 | $proffer->beforeSave( 915 | $this->createMock('Cake\Event\Event', null, ['beforeSave']), 916 | $entity, 917 | new ArrayObject() 918 | ); 919 | 920 | $this->assertEquals('image_640x480.jpg', $entity->get('photo')); 921 | $this->assertEquals('proffer_test', $entity->get('photo_dir')); 922 | 923 | $testUploadPath = $path->getFolder(); 924 | 925 | $this->assertFileExists($testUploadPath . 'image_640x480.jpg'); 926 | $this->assertFileExists($testUploadPath . 'portrait_' . 'image_640x480.jpg'); 927 | $this->assertFileExists($testUploadPath . 'square_' . 'image_640x480.jpg'); 928 | 929 | $portraitSizes = getimagesize($testUploadPath . 'portrait_' . 'image_640x480.jpg'); 930 | $this->assertEquals(100, $portraitSizes[0]); 931 | 932 | $squareSizes = getimagesize($testUploadPath . 'square_' . 'image_640x480.jpg'); 933 | $this->assertEquals(200, $squareSizes[0]); 934 | $this->assertEquals(200, $squareSizes[1]); 935 | } 936 | 937 | public function testMultipleFieldUpload() 938 | { 939 | $schema = $this->createMock(TableSchema::class); 940 | $table = $this->createMock(Table::class); 941 | $eventManager = $this->createMock(EventManager::class); 942 | $table->method('getAlias') 943 | ->willReturn('ProfferTest'); 944 | $table->method('getSchema') 945 | ->willReturn($schema); 946 | $table->method('getEventManager') 947 | ->willReturn($eventManager); 948 | 949 | $entityData = [ 950 | 'photo' => [ 951 | 'name' => 'image_640x480.jpg', 952 | 'tmp_name' => Plugin::path('Proffer') . 'tests' . DS . 'Fixture' . DS . 'image_640x480.jpg', 953 | 'size' => 33000, 954 | 'error' => UPLOAD_ERR_OK 955 | ], 956 | 'photo_dir' => 'proffer_test', 957 | 'avatar' => [ 958 | 'name' => 'image_480x640.jpg', 959 | 'tmp_name' => Plugin::path('Proffer') . 'tests' . DS . 'Fixture' . DS . 'image_480x640.jpg', 960 | 'size' => 45704, 961 | 'error' => UPLOAD_ERR_OK 962 | ], 963 | 'avatar_dir' => 'proffer_test' 964 | ]; 965 | $entity = new Entity($entityData); 966 | 967 | $config = [ 968 | 'photo' => [ 969 | 'dir' => 'photo_dir', 970 | 'thumbnailSizes' => [ 971 | 'square' => ['w' => 200, 'h' => 200, 'crop' => true], 972 | ], 973 | 'pathClass' => '\Proffer\Tests\Stubs\TestPath' 974 | ], 975 | 'avatar' => [ 976 | 'dir' => 'avatar_dir', 977 | 'thumbnailSizes' => [ 978 | 'square' => ['w' => 200, 'h' => 200, 'crop' => true], 979 | ], 980 | 'pathClass' => '\Proffer\Tests\Stubs\TestPath' 981 | ] 982 | ]; 983 | 984 | $proffer = $this->getMockBuilder(ProfferBehavior::class) 985 | ->setConstructorArgs([$table, $config]) 986 | ->setMethods(['moveUploadedFile']) 987 | ->getMock(); 988 | 989 | $proffer->expects($this->exactly(2)) 990 | ->method('moveUploadedFile') 991 | ->willReturnCallback(function ($source, $destination) { 992 | if (!file_exists(pathinfo($destination, PATHINFO_DIRNAME))) { 993 | mkdir(pathinfo($destination, PATHINFO_DIRNAME), 0777, true); 994 | } 995 | 996 | return copy($source, $destination); 997 | }); 998 | 999 | $proffer->beforeSave( 1000 | $this->createMock(Event::class), 1001 | $entity, 1002 | new ArrayObject() 1003 | ); 1004 | 1005 | $this->assertFileExists(TMP . 'ProfferTests' . DS . 'proffertest' . DS . 'photo' . 1006 | DS . 'proffer_test' . DS . 'image_640x480.jpg'); 1007 | $this->assertFileExists(TMP . 'ProfferTests' . DS . 'proffertest' . DS . 'avatar' . 1008 | DS . 'proffer_test' . DS . 'image_480x640.jpg'); 1009 | 1010 | $this->assertEquals('image_640x480.jpg', $entity->get('photo')); 1011 | $this->assertEquals('proffer_test', $entity->get('photo_dir')); 1012 | 1013 | $this->assertEquals('image_480x640.jpg', $entity->get('avatar')); 1014 | $this->assertEquals('proffer_test', $entity->get('avatar_dir')); 1015 | } 1016 | 1017 | /** 1018 | * Test that uploads are processed correctly when the upload is it's own entity. For when users associate many 1019 | * uploads with a single parent item. Such as Posts hasMany Uploads 1020 | * 1021 | * @return void 1022 | */ 1023 | public function testMultipleAssociatedUploads() 1024 | { 1025 | $eventManager = $this->createMock(EventManager::class); 1026 | 1027 | $uploadsSchema = $this->getMockBuilder(TableSchema::class) 1028 | ->setConstructorArgs([ 1029 | 'uploads', 1030 | [ 1031 | 'photo' => 'string', 1032 | 'photo_dir' => 'string' 1033 | ] 1034 | ]) 1035 | ->getMock(); 1036 | 1037 | $uploadsTable = $this->getMockBuilder(Table::class) 1038 | ->setConstructorArgs([ 1039 | ['schema' => $uploadsSchema] 1040 | ]) 1041 | ->getMock(); 1042 | 1043 | $uploadsTable->method('getEntityClass')->willReturn(Entity::class); 1044 | $uploadsTable->method('getAlias')->willReturn('Uploads'); 1045 | $uploadsTable->method('getSchema')->willReturn($uploadsSchema); 1046 | $uploadsTable->method('getEventManager')->willReturn($eventManager); 1047 | 1048 | $config = [ 1049 | 'photo' => [ 1050 | 'dir' => 'photo_dir', 1051 | 'thumbnailSizes' => [ 1052 | 'square' => ['w' => 200, 'h' => 200, 'crop' => true], 1053 | ], 1054 | 'pathClass' => '\Proffer\Tests\Stubs\TestPath' 1055 | ], 1056 | ]; 1057 | 1058 | $proffer = $this->getMockBuilder(ProfferBehavior::class) 1059 | ->setConstructorArgs([$uploadsTable, $config]) 1060 | ->setMethods(['moveUploadedFile']) 1061 | ->getMock(); 1062 | 1063 | $proffer->expects($this->exactly(1)) 1064 | ->method('moveUploadedFile') 1065 | ->willReturnCallback(function ($source, $destination) { 1066 | if (!file_exists(pathinfo($destination, PATHINFO_DIRNAME))) { 1067 | mkdir(pathinfo($destination, PATHINFO_DIRNAME), 0777, true); 1068 | } 1069 | 1070 | return copy($source, $destination); 1071 | }); 1072 | 1073 | $entity = new Entity([ 1074 | 'name' => 'image_640x480.jpg', 1075 | 'tmp_name' => Plugin::path('Proffer') . 'tests' . DS . 'Fixture' . DS . 'image_640x480.jpg', 1076 | 'size' => 33000, 1077 | 'error' => UPLOAD_ERR_OK 1078 | ]); 1079 | 1080 | $proffer->beforeSave( 1081 | $this->getMockBuilder(Event::class) 1082 | ->setConstructorArgs(['Model.beforeSave']) 1083 | ->getMock(), 1084 | $entity, 1085 | new ArrayObject() 1086 | ); 1087 | 1088 | $this->assertFileExists(TMP . 'ProfferTests' . DS . 'uploads' . DS . 'photo' . DS . 'proffer_test' . DS . 'image_640x480.jpg'); 1089 | 1090 | $this->assertEquals('image_640x480.jpg', $entity->get('photo')); 1091 | $this->assertEquals('proffer_test', $entity->get('photo_dir')); 1092 | } 1093 | } 1094 | -------------------------------------------------------------------------------- /tests/TestCase/Model/Validation/ProfferRulesTest.php: -------------------------------------------------------------------------------- 1 | loadPlugins(['Proffer' => ['path' => ROOT]]); 13 | } 14 | 15 | public function providerDimensions() 16 | { 17 | return [ 18 | [ 19 | ['tmp_name' => ROOT . 'tests' . DS . 'Fixture' . DS . 'image_640x480.jpg'], 20 | [ 21 | 'min' => ['w' => 100, 'h' => 100], 22 | 'max' => ['w' => 500, 'h' => 500] 23 | ], 24 | false 25 | ], 26 | [ 27 | ['tmp_name' => ROOT . 'tests' . DS . 'Fixture' . DS . 'image_640x480.jpg'], 28 | [ 29 | 'min' => ['w' => 700, 'h' => 500], 30 | 'max' => ['w' => 1000, 'h' => 800] 31 | ], 32 | false 33 | ], 34 | [ 35 | ['tmp_name' => ROOT . 'tests' . DS . 'Fixture' . DS . 'image_640x480.jpg'], 36 | [ 37 | 'min' => ['w' => 100, 'h' => 100], 38 | 'max' => ['w' => 700, 'h' => 700] 39 | ], 40 | true 41 | ], 42 | ]; 43 | } 44 | 45 | /** 46 | * @dataProvider providerDimensions 47 | */ 48 | public function testDimensions($value, $dimensions, $expected) 49 | { 50 | $result = ProfferRules::dimensions($value, $dimensions); 51 | $this->assertEquals($expected, $result); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | register(); 28 | $loader->addNamespace('Cake\Test\Fixture', ROOT . '/vendor/cakephp/cakephp/tests/Fixture'); 29 | 30 | require_once CORE_PATH . 'config/bootstrap.php'; 31 | 32 | date_default_timezone_set('UTC'); 33 | mb_internal_encoding('UTF-8'); 34 | 35 | Configure::write('debug', true); 36 | 37 | Configure::write('App', [ 38 | 'namespace' => 'App', 39 | 'encoding' => 'UTF-8', 40 | 'base' => false, 41 | 'baseUrl' => false, 42 | 'dir' => 'src', 43 | 'webroot' => 'webroot', 44 | 'www_root' => APP . 'webroot', 45 | 'fullBaseUrl' => 'http://localhost', 46 | 'imageBaseUrl' => 'img/', 47 | 'jsBaseUrl' => 'js/', 48 | 'cssBaseUrl' => 'css/', 49 | 'paths' => [ 50 | 'plugins' => [APP . 'Plugin' . DS], 51 | 'templates' => [APP . 'Template' . DS] 52 | ] 53 | ]); 54 | 55 | Configure::write('Session', [ 56 | 'defaults' => 'php' 57 | ]); 58 | 59 | Cache::setConfig([ 60 | '_cake_core_' => [ 61 | 'engine' => 'File', 62 | 'prefix' => 'cake_core_', 63 | 'serialize' => true 64 | ], 65 | '_cake_model_' => [ 66 | 'engine' => 'File', 67 | 'prefix' => 'cake_model_', 68 | 'serialize' => true 69 | ], 70 | 'default' => [ 71 | 'engine' => 'File', 72 | 'prefix' => 'default_', 73 | 'serialize' => true 74 | ] 75 | ]); 76 | 77 | // Ensure default test connection is defined 78 | if (!getenv('db_class')) { 79 | putenv('db_class=Cake\Database\Driver\Sqlite'); 80 | putenv('db_dsn=sqlite::memory:'); 81 | } 82 | 83 | ConnectionManager::setConfig('test', [ 84 | 'className' => 'Cake\Database\Connection', 85 | 'driver' => getenv('db_class'), 86 | 'dsn' => getenv('db_dsn'), 87 | 'database' => getenv('db_database'), 88 | 'username' => getenv('db_login'), 89 | 'password' => getenv('db_password'), 90 | 'timezone' => 'UTC' 91 | ]); 92 | 93 | Log::setConfig([ 94 | 'debug' => [ 95 | 'engine' => 'Cake\Log\Engine\FileLog', 96 | 'levels' => ['notice', 'info', 'debug'], 97 | 'file' => 'debug', 98 | ], 99 | 'error' => [ 100 | 'engine' => 'Cake\Log\Engine\FileLog', 101 | 'levels' => ['warning', 'error', 'critical', 'alert', 'emergency'], 102 | 'file' => 'error', 103 | ] 104 | ]); 105 | --------------------------------------------------------------------------------