├── .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 | 
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 | [](https://travis-ci.org/davidyell/CakePHP3-Proffer)
35 | [](https://coveralls.io/r/davidyell/CakePHP3-Proffer)
36 | [](https://www.versioneye.com/user/projects/54eee43931e55e12f9000018)
37 | [](https://packagist.org/packages/davidyell/proffer) [](https://packagist.org/packages/davidyell/proffer) [](https://packagist.org/packages/davidyell/proffer) [](https://packagist.org/packages/davidyell/proffer)
38 | [](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 |
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 |
--------------------------------------------------------------------------------