├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── docs ├── attachments.md ├── configuration.md ├── contributing.md ├── examples.md ├── imageprocessing.md ├── interpolations.md ├── setup.md └── troubleshooting.md ├── phpunit.xml ├── provides.json ├── src ├── Attachment.php ├── AttachmentConfig.php ├── Config │ └── NativeConfig.php ├── Exceptions │ ├── FileException.php │ ├── InvalidAttachmentConfigurationException.php │ ├── InvalidStyleConfigurationException.php │ ├── InvalidUrlOptionException.php │ └── UnknownFileExtensionException.php ├── Factories │ ├── Attachment.php │ ├── File.php │ └── Storage.php ├── File │ ├── File.php │ ├── Image │ │ └── Resizer.php │ └── UploadedFile.php ├── Interfaces │ ├── Attachment.php │ ├── Config.php │ ├── File.php │ ├── Interpolator.php │ ├── Resizer.php │ ├── Storage.php │ ├── Style.php │ └── Validator.php ├── Interpolator.php ├── ORM │ ├── EloquentTrait.php │ └── StaplerableInterface.php ├── Stapler.php ├── Storage │ ├── Filesystem.php │ └── S3.php ├── Style.php └── Validator.php └── tests ├── .gitkeep ├── Codesleeve └── Stapler │ ├── AttachmentConfigTest.php │ ├── AttachmentTest.php │ ├── Config │ └── NativeConfigTest.php │ ├── Factories │ ├── AttachmentTest.php │ ├── FileTest.php │ └── StorageTest.php │ ├── File │ ├── Image │ │ └── ResizerTest.php │ └── UploadedFileTest.php │ ├── Fixtures │ ├── Models │ │ └── Photo.php │ └── empty.gif │ ├── InterpolatorTest.php │ ├── StaplerTest.php │ └── StyleTest.php └── bootstrap.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /.idea 3 | composer.phar 4 | composer.lock 5 | .DS_Store 6 | phpunit.phar 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.5 5 | - 5.6 6 | - hhvm 7 | 8 | before_install: 9 | - composer self-update 10 | 11 | install: 12 | - composer install --dev 13 | 14 | script: phpunit 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | LICENSE 2 | 3 | The MIT License 4 | 5 | Copyright (c) 2014 Travis Bennett 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #Stapler 2 | [![Build Status](https://travis-ci.org/CodeSleeve/stapler.png?branch=master)](https://travis-ci.org/CodeSleeve/stapler) 3 | [![Latest Stable Version](https://poser.pugx.org/codesleeve/stapler/v/stable.svg)](https://packagist.org/packages/codesleeve/stapler) 4 | [![Total Downloads](https://poser.pugx.org/codesleeve/stapler/downloads.svg)](https://packagist.org/packages/codesleeve/stapler) 5 | [![Latest Unstable Version](https://poser.pugx.org/codesleeve/stapler/v/unstable.svg)](https://packagist.org/packages/codesleeve/stapler) 6 | [![License](https://poser.pugx.org/codesleeve/stapler/license.svg)](https://packagist.org/packages/codesleeve/stapler) 7 | 8 | **Note**: *If you've previously been using this package, then you've been using it with Laravel. This package is no longer directly coupled to the Laravel framework. As of 1.0.0, Stapler is now framework agnostic. In order to take advantage of the Laravel specific features provided by the previous Beta releases (service providers, IOC container, commands, migration generator, etc) , I've created a separate package specifically for the purpose of using Stapler within Laravel: [Laravel-Stapler](https://github.com/CodeSleeve/laravel-stapler). If you're using Stapler inside a Laravel application I strongly recommend you use this package (it will save you a bit of boilerplate).* 9 | 10 | Stapler is a php-based framework agnostic file upload package inspired by the [Ruby Paperclip](https://github.com/thoughtbot/paperclip) gem. It can be used to add file file uploads (as attachment objects) to your ORM records. While not an exact duplicate, if you've used Paperclip before then you should feel quite comfortable using this package. 11 | 12 | Stapler was created by [Travis Bennett](https://twitter.com/tandrewbennett). 13 | 14 | ## Requirements 15 | Stapler currently requires php >= 5.4 (Stapler is implemented via the use of traits). 16 | 17 | ## Installation 18 | Stapler is distributed as a composer package, which is how it should be used in your app. 19 | 20 | Install the package using Composer. Edit your project's `composer.json` file to require `codesleeve/stapler`. 21 | 22 | ```js 23 | "require": { 24 | "codesleeve/stapler": "1.0.*" 25 | } 26 | ``` 27 | 28 | ## About Stapler 29 | Stapler works by attaching file uploads to database table records. This is done by defining attachments inside the table's corresponding model and then assigning uploaded files (from your forms) as properties (named after the attachments) on the model before saving it. Stapler will listen to the life cycle callbacks of the model (after save, before delete, and after delete) and handle the file accordingly. In essence, this allows uploaded files to be treated just like any other property on the model; stapler will abstract away all of the file processing, storage, etc so you can focus on the rest of your project without having to worry about where your files are at or how to retrieve them. 30 | 31 | ### Key Benefits 32 | * **Modern**: Stapler runs on top of php >= 5.4 and takes advantage of many of the new features provided by modern php (traits, callable typehinting, etc). 33 | * **Simple**: Traditionally, file uploading has been known to be an arduous task; Stapler reduces much of the boilerplate required throughout this process. Seriously, Stapler makes it dead simple to get up and running with file uploads (of any type). 34 | * **Flexible**: Stapler provides an extremely flexible cascading configuration; files can be configured for storage locally or via AWS S3 by changing only a single configuration option. 35 | * **Scalable**: Storing your assets in a central location (such as S3) allows your files to be accessable by multiple web instances from a single location. 36 | * **Powerful**: Stapler makes use of modern object oriented programming patterns in order to provide a rock solid architecture for file uploading. It's trait-based driver system provides the potential for it to work across multiple ORMS (both Active Record and Data Mapper implementations) that implement life cycle callbacks. 37 | 38 | ## Documentation 39 | * [Setup](docs/setup.md) 40 | * [Configuration](docs/configuration.md) 41 | * [Interpolations](docs/interpolations.md) 42 | * [Image Processing](docs/imageprocessing.md) 43 | * [Working with Attachments](docs/attachments.md) 44 | * [Examples](docs/examples.md) 45 | * [Troubleshooting](docs/troubleshooting.md) 46 | * [Contributing](docs/contributing.md) 47 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codesleeve/stapler", 3 | "description": "Elegant and simple ORM-based file upload package for php.", 4 | "keywords": ["ORM", "file", "upload", "S3", "AWS", "paperclip"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Travis Bennett", 9 | "email": "tandrewbennett@hotmail.com" 10 | } 11 | ], 12 | "require": { 13 | "php": ">=5.4", 14 | "symfony/http-foundation": "~2.3|~3.0", 15 | "imagine/imagine": "~0.6.2", 16 | "doctrine/inflector": "~1" 17 | }, 18 | "require-dev": { 19 | "mockery/mockery": "0.8.0", 20 | "aws/aws-sdk-php": "2.4.*@dev", 21 | "illuminate/database": "4.*|5.*", 22 | "illuminate/config": "4.*|5.*" 23 | }, 24 | "autoload": { 25 | "psr-4": { 26 | "Codesleeve\\Stapler\\": "src/" 27 | } 28 | }, 29 | "minimum-stability": "dev" 30 | } 31 | -------------------------------------------------------------------------------- /docs/attachments.md: -------------------------------------------------------------------------------- 1 | ## Attachments 2 | Attachments are the bread and butter of Stapler. When you define an attached file on a model, your model automatically gains a new property containing an attachment value object for representing uploaded files on that record. Regardless or whether you've uploaded a file or not, this value object will exist. This allows file uploads to be represented in a simple yet powerful object oriented fashion. 3 | 4 | ### Properties 5 | * *Codesleeve\Stapler\ORM\StaplerableInterface* **instance**: The model instance that the attachment belongs to. 6 | * *Codesleeve\Stapler\AttachmentConfig* **config**: The attachment's config value object. 7 | * *Codesleeve\Stapler\Storage\StorageableInterface* **storageDriver**: An instance of the underlying storage driver being used by the attachment. 8 | * *Codesleeve\Stapler\Interpolator* **interpolator**: An instance of the interpolator class for processing interpolations. 9 | * *Codesleeve\Stapler\File\FileInterface* **uploadedFile**: The uploaded file object for the attachment. 10 | * *Codesleeve\Stapler\File\Image\Resizer* **resizer**: An instance of the resizer library that's being used for image processing. 11 | * *array* **queuedForDeletion**: An array of uploaded file objects queued up for deletion by Stapler. 12 | * *array* **queuedForWrite**: An array of uploaded file objects queued up to be written to storage by Stapler. 13 | 14 | ### Casting to JSON 15 | As of Stapler 1.2.0, Attachments implement the `JsonSerializable` interface and will now be automatically cast to a JSON object when called with `json_encode`. 16 | This JSON object contains the paths and urls for each style defined on the attachment: 17 | ```js 18 | "avatar": { 19 | "thumbnail": { 20 | "path": "path/to/foo/bar/baz/thumbnail/file.something", 21 | "url": "url/to/foo/bar/baz/thumbnail/file.something" 22 | }, 23 | "original": { 24 | "path": "path/to/foo/bar/baz/original/file.somethin", 25 | "url": "url/to/foo/bar/baz/thumbnail/file.something" 26 | } 27 | } 28 | ``` 29 | 30 | ### Methods 31 | Attachments contain an assortment of methods for working with uploaded files and their properties: 32 | 33 | * **setUploadedFile**: Mutator method for setting the uploadedFile property on the attachment. When a model is using stapler and a property value is set for one of the attachments defined on that model, this method is called. This allows allows files to be passed to stapler in multiple formats (strings, array, or symfony uploaded file objects) while ensuring that they're all converted to an instance of *Codesleeve\Stapler\File\FileInterface*. 34 | 35 | * **getUploadedFile**: Accessor method for the uploadedFile property on the attachment. Returns an instance of *Codesleeve\Stapler\File\FileInterface*. 36 | 37 | * **setInterpolator**: Mutator method for setting the interpolator property on the attachment. 38 | 39 | * **getInterpolator**: Accessor method for the interpolator property on the attachment. Returns an instance of *Codesleeve\Stapler\Interpolator*. 40 | 41 | * **setResizer**: Mutator method for setting the resizer property on the attachment. 42 | 43 | * **getResizer**: Accessor method for the resizer property on the attachment. Returns an instance of *Codesleeve\Stapler\File\Image\Resizer*. The resizer object is responsible for all of the geometry calculations, auto-orient calculations, etc that are done when an image is processed. 44 | 45 | * **setStorageDriver**: Mutator method for setting the storageDriver property on the attachment. 46 | 47 | * **getStorageDriver**: Accessor method for the storageDriver property on the attachment. Returns an instance of *Codesleeve\Stapler\Storage\StorageableInterface*. The storageDriver object is responsible handling the underlying storage of an uploaded file across the various storage mediums (file system, S3, etc). 48 | 49 | * **setInstance**: Mutator method for setting the instance property on the attachment. 50 | 51 | * **getInstance**: Accessor method for the instance property on the attachment. This is always the model/entity that the attachment was defined in and will vary depending upon which ORM/trait is currently being used. 52 | 53 | * **setConfig**: Mutator method for setting the config property on the attachment. 54 | 55 | * **getConfig**: Accessor method for the config property on the attachment. Configuration for attachment objects are stored in a value object of type *Codesleeve\Stapler\AttachmentConfig*. 56 | 57 | * **getQueuedForDeletion**: Accessor method for the queuedForDeletion property. 58 | 59 | * **getQueuedForWrite**: Accessor method for the queuedForWrite property. 60 | 61 | * **url**: Generates the url to an uploaded file (or a resized version of it). 62 | 63 | * **path**: Generates the file system path to an uploaded file (or a resized version of it). 64 | 65 | * **createdAt**: Returns the creation time of the file as originally assigned to this attachment's model. Lives in the _created_at attribute of the model. This attribute may conditionally exist on the model, it is not one of the four required fields. 66 | 67 | * **updatedAt**: Returns the last modified time of the file as originally assigned to this attachment's model. Lives in the _updated_at attribute of the model. 68 | 69 | * **contentType**: Returns the content type of the file as originally assigned to this attachment's model. Lives in the _content_type attribute of the model. 70 | 71 | * **size**: Returns the size of the file as originally assigned to this attachment's model. Lives in the _file_size attribute of the model. 72 | 73 | * **originalFilename**: Returns the name of the file as originally assigned to this attachment's model. Lives in the _file_name attribute of the model. 74 | 75 | * **getInstanceClass**: Returns the class type of the attachment's underlying model instance. 76 | 77 | * **reprocess**: Rebuilds the images for an attachment. This is an extremely powerful method; once called on an attachment object, it uses the original copy of the uploaded file to reprocess any styles defined on the attachment. This is extremely useful when adding a new style to an attachment that has already had a file uploaded and processed. 78 | 79 | * **afterSave**: This is the callback method triggered after an attachment's model instance has been saved. Once triggered, it causes the queuedForWrite array to be flushed, which in turn triggers image processing, file storage, etc. 80 | 81 | * **beforeDelete**: This is the callback method triggered before a model instance is deleted. Once triggered, all images/files for the attachment will be added to the queuedForDeletion array. 82 | 83 | * **afterDelete**: This is the callback method triggered after a model instance has been deleted. Once triggered, it causes the queuedForDeletion array to be flushed/processed. 84 | 85 | * **destroy**: Removes all uploaded files (from storage) for an attachment. This method does not clear out attachment attributes on the model instance. An array of styles can be passed so that only those styles are removed from storage. 86 | 87 | * **clear**: Queues up all or some of this attachments uploaded files/images for deletion. An array of styles can be passed so that only those styles are queued up for deletion. 88 | 89 | * **save**: Flushes the queuedForDeletion and queuedForWrite arrays. 90 | 91 | * **instanceWrite**: Set an attachment attribute on the underlying model instance. Accepts the name of an attachment property ('size', 'content_type', etc) as well as the value that should be set for the property. 92 | 93 | * **clearAttributes**: Clear (set to null) all attachment related model attributes. 94 | 95 | * **jsonSerialize**: Returns a json representation of an attachment. This is very useful when returning ORM instances as JSON and from an API. 96 | 97 | 98 | -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | ## Configuration 2 | Configuration is available on both a per attachment basis or globally through the configuration file settings. Stapler is very flexible about how it processes configuration; global configuration options can be overriden on a per attachment basis so that you can easily cascade settings you would like to have on all attachments while still having the freedom to customize an individual attachment's configuration. 3 | 4 | * [Stapler](#stapler-configuration) 5 | * [Filesystem](#filesystem-storage-configuration) 6 | * [S3](#s3-storage-configuration) 7 | 8 | ### Stapler-Configuration 9 | The following configuration settings apply to stapler in general. 10 | 11 | * **storage**: The underlying storage driver to uploaded files. Defaults to filesystem (local storage) but can also be set to 's3' for use with AWS S3. 12 | * **image_processing_libarary**: The underlying image processing library being used. Defaults to GD but can also be set to Imagick or Gmagick. 13 | * **default_url**: The default file returned when no file upload is present for a record. 14 | * **default_style**: The default style returned from the Stapler file location helper methods. An unaltered version of uploaded file 15 | is always stored within the 'original' style, however the default_style can be set to point to any of the defined syles within the styles array. 16 | * **styles**: An array of image sizes defined for the file attachment. Stapler will attempt to use to format the file upload 17 | into the defined style. 18 | * **keep_old_files**: Set this to true in order to prevent older file uploads from being deleted from the file system when a record is updated. 19 | * **preserve_old_files**: Set this to true in order to prevent an attachment's file uploads from being deleted from the file system when an the attachment object is destroyed (attachment's are destroyed when their corresponding mondels are deleted/destroyed from the database). 20 | * **convert_options**: An array of options for setting the quality and DPI of resized images. Default values are 75 for Jpeg quality and 72 dpi for x/y-resolution. Please see the Imagine\Image documentation for more details. 21 | 22 | Default values: 23 | * **storage**: 'filesystem' 24 | * **image_processing_library**: 'GD' 25 | * **default_url**: '/:attachment/:style/missing.png' 26 | * **default_style**: 'original' 27 | * **styles**: [] 28 | * **keep_old_files**: false 29 | * **preserve_old_files**: false 30 | * **convert_options**: [] 31 | 32 | ### Filesystem-Storage-Configuration 33 | Filesystem (local disk) is the default storage option for stapler. When using it, the following configuration settings are available: 34 | 35 | * **url**: The url (relative to your project document root) where files will be stored. It is composed of 'interpolations' that will be replaced their corresponding values during runtime. It's unique in that it functions as both a configuration option and an interpolation. 36 | * **path**: Similar to the url, the path option is the location where your files will be stored at on disk. It should be noted that the path option should not conflict with the url option. Stapler provides sensible defaults that take care of this for you. 37 | * **override_file_permissions**: Override the default file permissions used by stapler when creating a new file in the file system. Leaving this value as null will result in stapler chmod'ing files to 0666. Set it to a specific octal value and stapler will chmod accordingly. Set it to false to prevent chmod from occuring (useful for non unix-based environments). 38 | 39 | Default values: 40 | * **url**: '/system/:class/:attachment/:id_partition/:style/:filename' 41 | * **path**: ':laravel_root/public:url' 42 | * **override_file_permissions**: null 43 | 44 | ### S3-Storage-Configuration 45 | **Note**: *The current release of Stapler only supports AWS SDK ~2. This is not going to change until the next major release comes out (due to backwards compatibility breaks).* 46 | 47 | As your web application grows, you may find yourself in need of more robust file storage than what's provided by the local filesystem (e.g you're using multiple server instances and need a shared location for storing/accessing uploaded file assets). Stapler provides a simple mechanism for easily storing and retreiving file objects with Amazon Simple Storage Service (Amazon S3). In fact, aside from a few extra configuration settings, there's really no difference between s3 storage and filesystem storage when interacting with your attachments. To get started with s3 storage you'll first need to add the AWS SDK to your composer.json file: 48 | 49 | ```js 50 | "require": { 51 | "codesleeve/stapler": 52 | "dev-master", 53 | "aws/aws-sdk-php": "2.4.*@dev" 54 | } 55 | ``` 56 | 57 | Next, change the storage setting in config/stapler.php from 'filesystem' to 's3' (keep in mind, this can be done per attachment if you want to use s3 for a specific attachment only). As of Stapler 1.0.0, S3 storage configuration for the S3Client is broken down into two arrays: 58 | 59 | * **s3_client_config**: An array of key/value pairs that will be passed directly into the S3Client::factory() method. You can go [here](http://docs.aws.amazon.com/aws-sdk-php/guide/latest/configuration.html#client-configuration-options) for a complete list/explanation of these options. 60 | * **s3_object_config**: An array of key/value pairs that will be passed directly to the S3Client::putObject() method. You can go [here](http://docs.aws.amazon.com/aws-sdk-php/latest/class-Aws.S3.S3Client.html#_putObject) for a complete list/explanation of these options. 61 | 62 | Description: 63 | * **s3_client_config** 64 | * **key**: This is an alphanumeric text string that uniquely identifies the user who owns the account. No two accounts can have the same AWS Access Key. 65 | * **secret**: This key plays the role of a password . It's called secret because it is assumed to be known by the owner only. A Password with Access Key forms a secure information set that confirms the user's identity. You are advised to keep your Secret Key in a safe place. 66 | * **region**: The region name of your bucket (e.g. 'us-east-1', 'us-west-1', 'us-west-2', 'eu-west-1'). Determines the base url where your objects are stored at (e.g a region of us-west-2 has a base url of s3-us-west-2.amazonaws.com). The default value for this field is an empty (US Standard *). You can go [here](http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region) for a more complete list/explanation of regions. 67 | * **scheme**: The protocol for the URLs generated to your S3 assets. Can be either 'http' or 'https'. Defaults to 'http' when your ACL is 'public-read' (the default) and 'https' when your ACL is anything else. 68 | * **s3_object_config** 69 | * **Bucket**: The bucket where you wish to store your objects. Every object in Amazon S3 is stored in a bucket. If the specified bucket doesn't exist Stapler will attempt to create it. The bucket name will not be interpolated. 70 | * **ACL**: This is a string/array that should be one of the canned access policies that S3 provides (private, public-read, public-read-write, authenticated-read, bucket-owner-read, bucket-owner-full-control). The default for Stapler is public-read. An associative array (style => permission) may be passed to specify permissions on a per style basis. 71 | * **path**: This is the key under the bucket in which the file will be stored. The URL will be constructed from the bucket and the path. This is what you will want to interpolate. Keys should be unique, like filenames, and despite the fact that S3 (strictly speaking) does not support directories, you can still use a / to separate parts of your file name. 72 | 73 | Default values: 74 | * **s3_client_config** 75 | * **key**: '' 76 | * **secret**: '' 77 | * **region**: '' 78 | * **scheme**: 'http' 79 | * **s3_object_config** 80 | * **Bucket**: '' 81 | * **ACL**: 'public-read' 82 | * **path**: ':attachment/:id/:style/:filename' -------------------------------------------------------------------------------- /docs/contributing.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | This package is always open to contributions: 3 | 4 | * Master will always contain the newest work (bug fixes, new features, etc), however it may not always be stable; use at your own risk. Every new tagged release will come from the work done on master, once things have stablized, etc. 5 | 6 | ### Formating 7 | I'm very particular about the way I format my php code. In general, if you're submitting a pull request to Stapler, please adhere to the following guidelines and conventions: 8 | 9 | If statements should always be wrapped in curly braces and contain a single space on both sides the parens. If there is only a single line of code to be executed, please put the first curly brace on the same line as the condition: 10 | 11 | ```php 12 | if (true) { 13 | $foo = $bar; 14 | } 15 | ``` 16 | 17 | If there is more than one statement to be executed, each curly brace should appear on its own line: 18 | ```php 19 | if (true) 20 | { 21 | $foo = $bar; 22 | $baz = $qux; 23 | } 24 | ``` 25 | 26 | This formatting also applies to loops: 27 | ```php 28 | foreach ($foo as $bar) { 29 | $baz = $qux; 30 | } 31 | 32 | foreach ($foo as $bar) 33 | { 34 | $baz = $qux; 35 | $quux = $corge; 36 | } 37 | ``` 38 | 39 | File and class names should always be camel cased. Namespace and class declarations should always look like the following: 40 | ```php 41 | 50 | ``` 51 | 52 | Functions should always include docblock headers with each curly brace on a new line: 53 | ```php 54 | /** 55 | * A brief description of what the foo function does. 56 | * 57 | * @param string $name 58 | * @param array $baz 59 | */ 60 | function foo ($bar, array $baz) 61 | { 62 | //code 63 | } 64 | ``` 65 | 66 | If a function has a return value, its type should also be listed in the docblock (the @return annotation should be omitted if there is no return value): 67 | ```php 68 | /** 69 | * A brief description of what the foo function does. 70 | * 71 | * @param string $name 72 | * @param array $baz 73 | * @return array 74 | */ 75 | function foo ($bar, array $baz) 76 | { 77 | return $baz; 78 | } 79 | ``` 80 | 81 | Variables should always be named using camelback syntax and should be expressive of the data they contain: 82 | 83 | ```php 84 | $firstName = 'Travis'; 85 | $lastName = 'Bennett'; 86 | ``` -------------------------------------------------------------------------------- /docs/examples.md: -------------------------------------------------------------------------------- 1 | ## Examples 2 | * [Eloquent](#eloquent) 3 | * [Defining Attachments](#defining-attachments) 4 | * [Saving Files](#saving-files) 5 | * [Retreiving Uploads](#retreiving-uploads) 6 | * [Deleting uploads](#deleting-uploads) 7 | 8 | *These examples assume you have already booted Stapler (see [setup](setup.md) for more info on this).* 9 | 10 | ### Eloquent 11 | #### Definining-Attachments 12 | ```php 13 | use Codesleeve\Stapler\ORM\StaplerableInterface; 14 | use Codesleeve\Stapler\ORM\EloquentTrait; 15 | 16 | Class Photo extends Eloquent implements StaplerableInterface 17 | { 18 | // We'll need to use the Stapler Eloquent trait in our model (see setup for more info). 19 | use EloquentTrait; 20 | 21 | /** 22 | * We can add our attachments to the fillable array so that they're 23 | * mass assignable on the model. 24 | * 25 | * @var array 26 | */ 27 | protected $fillable = ['foo', 'bar', 'baz', 'qux', 'quux']; 28 | 29 | /** 30 | * Inside our model's constructor, we'll define some stapler attachments: 31 | * 32 | * @param attributes 33 | */ 34 | public function __construct(array $attributes = array()) 35 | { 36 | // Define an attachment named 'foo', with both thumbnail (100x100) and large (300x300) styles, 37 | // using custom url and default_url configurations: 38 | $this->hasAttachedFile('foo', [ 39 | 'styles' => [ 40 | 'thumbnail' => '100x100', 41 | 'large' => '300x300' 42 | ], 43 | 'url' => '/system/:attachment/:id_partition/:style/:filename', 44 | 'default_url' => '/:attachment/:style/missing.jpg' 45 | ]); 46 | 47 | // Define an attachment named 'bar', with both thumbnail (100x100) and large (300x300) styles, 48 | // using custom url and default_url configurations, with the keep_old_files flag set to true 49 | // (so that older file uploads aren't deleted from the file system) and image cropping turned on: 50 | $this->hasAttachedFile('bar', [ 51 | 'styles' => [ 52 | 'thumbnail' => '100x100#', 53 | 'large' => '300x300#' 54 | ], 55 | 'url' => '/system/:attachment/:id_partition/:style/:filename', 56 | 'keep_old_files' => true 57 | ]); 58 | 59 | // Define an attachment named 'baz' that has a watermarked style. Here, we define a style named 'watermarked' 60 | // that's a closure (so that we can do some complex watermarking stuff): 61 | $this->hasAttachedFile('baz', [ 62 | 'styles' => [ 63 | 'thumbnail' => ['dimensions' => '100x100', 'auto-orient' => true, 'convert_options' => ['quality' => 100]], 64 | 'micro' => '50X50', 65 | 'watermarked' => function($file, $imagine) { 66 | $watermark = $imagine->open('/path/to/images/watermark.png'); // Create an instance of ImageInterface for the watermark image. 67 | $image = $imagine->open($file->getRealPath()); // Create an instance of ImageInterface for the uploaded image. 68 | $size = $image->getSize(); // Get the size of the uploaded image. 69 | $watermarkSize = $watermark->getSize(); // Get the size of the watermark image. 70 | 71 | // Calculate the placement of the watermark (we're aiming for the bottom right corner here). 72 | $bottomRight = new Imagine\Image\Point($size->getWidth() - $watermarkSize->getWidth(), $size->getHeight() - $watermarkSize->getHeight()); 73 | 74 | // Paste the watermark onto the image. 75 | $image->paste($watermark, $bottomRight); 76 | 77 | // Return the Imagine\Image\ImageInterface instance. 78 | return $image; 79 | } 80 | ], 81 | 'url' => '/system/:attachment/:id_partition/:style/:filename' 82 | ]); 83 | 84 | // Define an attachment named 'qux'. In this attachment, we'll use alternative style notation to define a slightly more 85 | // complex thumbnail style. In this example, the thumbnail style will be a 100x100px auto-oriented image with 100% quality: 86 | $this->hasAttachedFile('qux', [ 87 | 'styles' => [ 88 | 'thumbnail' => ['dimensions' => '100x100', 'auto-orient' => true, 'convert_options' => ['quality' => 100]], 89 | 'micro' => '50X50' 90 | ], 91 | 'url' => '/system/:attachment/:id_partition/:style/:filename', 92 | 'default_url' => '/defaults/:style/missing.png' 93 | ]); 94 | 95 | // Define an attachment named 'quux' that stores images remotely in an S3 bucket. 96 | $this->hasAttachedFile('quux', [ 97 | 'styles' => [ 98 | 'thumbnail' => '100x100#', 99 | 'large' => '300x300#' 100 | ], 101 | 'storage' => 's3', 102 | 's3_client_config' => [ 103 | 'key' => 'yourPublicKey', 104 | 'secret' => 'yourSecreteKey', 105 | 'region' => 'yourBucketRegion' 106 | ], 107 | 's3_object_config' => [ 108 | 'Bucket' => 'your.s3.bucket' 109 | ], 110 | 'default_url' => '/defaults/:style/missing.png', 111 | 'keep_old_files' => true 112 | ]); 113 | 114 | // IMPORTANT: the call to the parent constructor method 115 | // should always come after we define our attachments. 116 | parent::__construct($attributes); 117 | } 118 | } 119 | ``` 120 | 121 | #### Saving-Files 122 | Once an attachment is defined on a model, we can then assign values to it (as a property on the model) in order to save it as a file upload. Assuming we had an instance of our Photo model from above, we can assign a value to any of our defined attachments before saving the model. Upon a successful save of the record, Stapler will go in and handle all of the file uploading, image processing, etc for us. In a controller somewhere, let's assume that we've fetched (or created) a photo model instance and we want to assign some file values to it (from a previously submitted form): 123 | 124 | ```php 125 | // If we're using Laravel, we can assign the Symfony uploaded file object directly on the modeal: 126 | $photo->foo = Input::file('foo'); 127 | $photos->save(); 128 | 129 | // In fact, because our attachments are listed in our fillable array, we can simple mass assign all input values on our photo: 130 | $photo->fill(Input::all()); 131 | $photo->save(); 132 | 133 | // If we're not using Laravel, we can assign an array (from the $_FILES array) to the uploaded file: 134 | $photo->foo = $_FILES['foo']; 135 | $photo->save(); 136 | 137 | // Regardless of what framework we're using, we can always assign a remote url as an attachment value. 138 | // This is very useful when working with third party API's such as facebook, twitter, etc. 139 | // Note that this feature requires that the CURL extension is included as part of your PHP installation. 140 | $photo->foo = "http://foo.com/bar.jpg"; 141 | $photo->save(); 142 | 143 | // Or an existing file on the local filesystem: 144 | $photo->foo = "/some/path/on/the/local/file/system/bar.jpg"; 145 | $photo->save(); 146 | ``` 147 | 148 | #### Retreiving-Uploads 149 | After we define an attachment on a model, we can access the attachment as a property on the model (regardless of whether or not an image has been uploaded or not). When attempting to display images, the default image url will be displayed until an image is uploaded. The attachment itself is an instance of Codesleeve\Stapler\Attachment (see [attachments](attachments.md) for more info on attachments). An attachment is really just a value object; it provides methods for seamlessly accessing the properties, paths, and urls of the underlying uploaded file. Continuing our example from above, lets assume we wanted to display the various styles of our previously defined foo attachment in an image tag. Assuming we had an instance of the Photo model, we could do the following: 150 | ```html 151 | Display a resized thumbnail style image belonging to a user record 152 | 153 | 154 | Display the original image style (unmodified image): 155 | 156 | 157 | This also displays the unmodified original image (unless the :default_style interpolation has been set to a different style): 158 | 159 | ``` 160 | 161 | As you can see, we can display any of the defined styles for a given attachment. We can also retrieve the full file path (on disk) of a given style (this is very useful when providing file download functionality): 162 | ```php 163 | $photo->foo->path('thumbnail'); 164 | ``` 165 | 166 | We can also grab the size, original filename, laste updated timestamp, and content type of the original (unaltered) uploaded file (**NOTE**: *stapler will always store an unaltered version of the original file*): 167 | ```php 168 | $photo->foo->size(); 169 | $photo->foo->originalFilename(); 170 | $photo->foo->updatedAt(); 171 | $photo->foo->contentType(); 172 | ``` 173 | 174 | #### Deleting-Uploads 175 | Unless you've set the 'keep_old_files' flag on the attachment to true, deleting a record will automatically remove all uploaded files, across all attachments, across all styles, for the a given model/record: 176 | ```php 177 | $photo->delete(); 178 | ``` 179 | 180 | If we need to remove the uploaded files only (the photo record itself will remain intact), we can assign the attachment a value of STAPLER_NULL and then save the record. This will remove all of the attachment's uploaded files from storage and clear out the attachment related file attributes on the model: 181 | ```php 182 | // Remove all of the attachment's uploaded files and empty the attacment attributes on the model (does not save the record though). 183 | $photo->foo = STAPLER_NULL; 184 | $photo->save(); 185 | ``` 186 | 187 | The destroy method is similar, however it doesn't clear out the attachment attributes on the model and doesn't require us to save the record in order to remove uploaded files. It's also filterable; we can pass in array of the syles we want to clear: 188 | ```php 189 | // Remove all of the attachments's uploaded files (across all styles) from storage. 190 | $photo->foo->destroy(); 191 | 192 | // Remove thumbnail files only. 193 | $photo->foo->destroy(['thumbnail']); 194 | ``` 195 | 196 | You may also reprocess uploaded images on an attachment by calling the reprocess() command (this is very useful for adding new styles to an existing attachment type where records have already been uploaded). 197 | 198 | ```php 199 | // Programmatically reprocess an attachment's uploaded images: 200 | $photo->foo->reprocess(); 201 | ``` -------------------------------------------------------------------------------- /docs/imageprocessing.md: -------------------------------------------------------------------------------- 1 | ## Image-Processing 2 | Stapler makes use of the [imagine image](https://packagist.org/packages/imagine/imagine) library for all image processing. Out of the box, the following image processing patterns/directives will be recognized when defining Stapler styles: 3 | 4 | * **width**: A style that defines a width only (landscape). Height will be automagically selected to preserve aspect ratio. This works well for resizing images for display on mobile devices, etc. 5 | * **xheight**: A style that defines a heigh only (portrait). Width automagically selected to preserve aspect ratio. 6 | * **widthxheight#**: Resize then crop. 7 | * **widthxheight!**: Resize by exacty width and height. Width and height emphatically given, original aspect ratio will be ignored. 8 | * **widthxheight**: Auto determine both width and height when resizing. This will resize as close as possible to the given dimensions while still preserving the original aspect ratio. 9 | 10 | To create styles for an attachment, simply define them (you may use any style name you like: foo, bar, baz, etc) inside the attachment's styles array using a combination of the directives defined above: 11 | 12 | ```php 13 | 'styles' => [ 14 | 'thumbnail' => '50x50', 15 | 'large' => '150x150', 16 | 'landscape' => '150', 17 | 'portrait' => 'x150', 18 | 'foo' => '75x75', 19 | 'fooCropped' => '75x75#' 20 | ] 21 | ``` 22 | 23 | To control the quality of resized images, define your style as an array containing 'dimensions' and 'convert_options' keys (**NOTE**: *when defining a style as an array, you must include a dimensions key. Stapler will throw an InvalidStyleConfigurationException otherwise*). The values for the convert_options array can be any of the quality settings described [here](https://imagine.readthedocs.org/en/latest/usage/introduction.html) for the Imagine Image package. 24 | 25 | ```php 26 | // Create a high quality jpeg thumbnail image. 27 | 'styles' => [ 28 | 'thumbnail' => [ 29 | 'dimensions' => '50x50', 30 | 'convert_options' => ['quality' => 100] 31 | ] 32 | ] 33 | ``` 34 | 35 | Using this syntax, we can also auto-orient images (**NOTE**: *this requires the exif extension as part or your php installation*). This is very useful for handling mobile uploads, etc. 36 | ```php 37 | // Create an auto-oriented jpeg thubmnail image. 38 | 'styles' => [ 39 | 'thumbnail' => [ 40 | 'dimensions' => '50x50', 41 | 'auto_orient' => true 42 | ] 43 | ] 44 | ``` 45 | 46 | Of course we can combine these options: 47 | ```php 48 | // Create an auto-oriented high quality jpeg thumbnail image. 49 | 'styles' => [ 50 | 'thumbnail' => [ 51 | 'dimensions' => '50x50', 52 | 'convert_options' => ['quality' => 100], 53 | 'auto_orient' => true 54 | ] 55 | ] 56 | ``` 57 | 58 | For even more customized image processing you may also pass a [callable](http://php.net/manual/en/language.types.callable.php) type as the value for a given style definition. Stapler will automatically inject in the uploaded file object instance as well as the Imagine\Image\ImagineInterface object instance for you to work with. When you're done with your processing, simply return an instance of Imagine\Image\ImageInterface from the callable. Using a callable for a style definition provides an incredible amount of flexibilty when it comes to image processing. As an example of this, let's create a watermarked image using a closure (we'll do a smidge of image processing with Imagine): 59 | 60 | ````php 61 | 'styles' => [ 62 | 'watermarked' => function($file, $imagine) { 63 | $watermark = $imagine->open('/path/to/images/watermark.png'); // Create an instance of ImageInterface for the watermark image. 64 | $image = $imagine->open($file->getRealPath()); // Create an instance of ImageInterface for the uploaded image. 65 | $size = $image->getSize(); // Get the size of the uploaded image. 66 | $watermarkSize = $watermark->getSize(); // Get the size of the watermark image. 67 | 68 | // Calculate the placement of the watermark (we're aiming for the bottom right corner here). 69 | $bottomRight = new Imagine\Image\Point($size->getWidth() - $watermarkSize->getWidth(), $size->getHeight() - $watermarkSize->getHeight()); 70 | 71 | // Paste the watermark onto the image. 72 | $image->paste($watermark, $bottomRight); 73 | 74 | // Return the Imagine\Image\ImageInterface instance. 75 | return $image; 76 | } 77 | ] 78 | ```` 79 | -------------------------------------------------------------------------------- /docs/interpolations.md: -------------------------------------------------------------------------------- 1 | ## Interpolations 2 | With Stapler, uploaded files are accessed by configuring/defining path, url, and default_url strings which point to your uploaded file assets. This is done via string interpolations. Currently, the following interpolations are available for use: 3 | 4 | * **:attachment** - The name of the file attachment as declared in the hasAttachedFile function, e.g 'avatar'. 5 | * **:class** - The class name of the model containing the file attachment, e.g User. This will include the class namespace. 6 | * **:class_name** - The class name of the model, without its namespace. 7 | * **:extension** - The file extension type of the uploaded file, e.g 'jpg'. 8 | * **:filename** - The name of the uploaded file, e.g 'some_file.jpg'. 9 | * **:id** - The id of the corresponding database record for the uploaded file. 10 | * **:id_partition** - The partitioned id of the corresponding database record for the uploaded file, e.g an id = 1 is interpolated as 000/000/001. This is the default and recommended setting for Stapler. Partioned id's help overcome the 32k subfolder problem that occurs in nix-based systems using the EXT3 file system. 11 | * **:secure_hash** - An sha256 hash of the corresponding database record id, the filesize, and the original file name. 12 | * **:hash** - An sha256 hash of the corresponding database record id. 13 | * **:app_root** - The path to the root of the project. 14 | * **:style** - The resizing style of the file (images only), e.g 'thumbnail' or 'original'. 15 | * **:url** - The url string pointing to your uploaded file. This interpolation is actually an interpolation itself. It can be composed of any of the above interpolations (except itself). 16 | 17 | As of stapler 1.1.0, the interpolator class is now bound to an interface/contract that's configurable via the 'bindings' array of the config file. 18 | If you need your own custom interpolations, you can easily swap out the default concrete implementation with your own. 19 | 20 | ```php 21 | 22 | use Codesleeve\Stapler\Interpolator as BaseInterpolator; 23 | 24 | class CustomerInterpolator extends BaseInterpolator 25 | { 26 | /** 27 | * Returns a sorted list of all interpolations. 28 | * We can easily add to the list of interpolations provided by 29 | * the base interpolator class. Let's register the ':foo' interpolation: 30 | * 31 | * @return array 32 | */ 33 | protected function interpolations() 34 | { 35 | $parentInterpolations = parent::interpolations(); 36 | 37 | return array_merge($parentInterpolations, [':foo' => 'foo']); 38 | } 39 | 40 | /** 41 | * Now that we've registered the 'foo' interpolation, we need 42 | * to implement the 'foo' function that generates the interpolated value 43 | * for us. Let's just replace 'foo' with the string 'bar': 44 | * 45 | * @param AttachmentInterface $attachment 46 | * @param string $styleName 47 | * 48 | * @return string 49 | */ 50 | protected function foo(AttachmentInterface $attachment, $styleName = '') 51 | { 52 | return 'bar'; 53 | } 54 | } 55 | ``` -------------------------------------------------------------------------------- /docs/setup.md: -------------------------------------------------------------------------------- 1 | ## Setup 2 | ### Bootstrapping 3 | Before you can begin using Stapler, there's a few things you're going to have to do. In your application's bootstrap process (wherever that may be), you're going to need to run the following code snippet: 4 | 5 | ```php 6 | // Boot stapler: 7 | \Codesleeve\Stapler\Stapler::boot(); 8 | 9 | // Set the configuration driver (we're using the default config driver here; if you choose to implement your own you'll need to implement Codesleeve\Stapler\Config\ConfigurableInterface): 10 | $config = new Codesleeve\Stapler\Config\NativeConfig; 11 | Stapler::setConfigInstance($config); 12 | 13 | // Set the location to your application's document root: 14 | $config->set('stapler.public_path', 'path/to/your/document/root'); 15 | 16 | // Set the location to your application's base folder. 17 | $config->set('stapler.base_path', 'path/to/your/base/folder'); 18 | ``` 19 | 20 | ### Traits/Drivers 21 | Stapler works via the use of traits. In order to add file uploading capabilities to your models/entities, you'll have to first use the corresponding trait for your ORM and ensure that your entities implement `Codesleeve\Stapler\ORM\StaplerableInterface`. Stapler currently supports the following ORMS (more coming soon): 22 | * Eloquent: A trait for use within Laravel's Eloquent ORM. Use this trait inside your Eloquent models in order to add file attachment abilities to them: 23 | ```php 24 | use Codesleeve\Stapler\ORM\StaplerableInterface; 25 | use Codesleeve\Stapler\ORM\EloquentTrait; 26 | 27 | class FooModel extends Eloquent implements StaplerableInterface{ 28 | use EloquentTrait; 29 | } 30 | ``` 31 | 32 | ### Database Tables 33 | A model can have multiple attachments defined (avatar, photo, foo, etc) and in turn each attachment can have multiple sizes (styles) defined. When an image or file is uploaded, Stapler will handle all the file processing (moving, resizing, etc) and provide an attachment object (as a model property) with methods for working with the uploaded file. To accomplish this, four fields (named after the attachment) will need to be created in the corresponding table for any model/entity containing a file attachment. For example, for an attachment named 'avatar' defined inside a model named 'User', the following fields would need to be added to the 'users' table: 34 | 35 | * (string) avatar_file_name 36 | * (integer) avatar_file_size 37 | * (string) avatar_content_type 38 | * (timestamp) avatar_updated_at 39 | -------------------------------------------------------------------------------- /docs/troubleshooting.md: -------------------------------------------------------------------------------- 1 | ## Troubleshooting 2 | > I get a Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException when attempting ot upload a file with stapler. 3 | 4 | Check your form to ensure that the **enctype** attribute is set to 'multipart/form-data'. If you're using Laravel's form helper to create your form, this can be done by adding 'files' => true to the form helper's open() method: 5 | 6 | ```php 7 | true]) ?> 8 | ``` 9 | 10 | > I'm using Stapler with Eloquent to upload my files. When I hit submit, the record gets saved, the attachment columns get set in the database, but no files are being uploaded. 11 | 12 | This is most likely happening because you've created a static 'boot' method inside your model and it's overriding the boot method used by Stapler's Eloquent Trait. In order to fix this, simply call the 'bootStapler' method from inside the boot method you defined: 13 | ```php 14 | /** 15 | * The "booting" method of the model. 16 | */ 17 | public static function boot() 18 | { 19 | parent::boot(); 20 | 21 | static::bootStapler(); 22 | } 23 | ``` -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | ./tests/Codesleeve/Stapler/ 16 | 17 | 18 | -------------------------------------------------------------------------------- /provides.json: -------------------------------------------------------------------------------- 1 | { 2 | "providers": [ 3 | "Codesleeve\Stapler\StaplerServiceProvider", 4 | ], 5 | "aliases" : [ 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /src/Attachment.php: -------------------------------------------------------------------------------- 1 | config = $config; 82 | $this->interpolator = $interpolator; 83 | $this->resizer = $resizer; 84 | } 85 | 86 | /** 87 | * Handle the dynamic setting of attachment options. 88 | * 89 | * @param string $name 90 | * @param mixed $value 91 | */ 92 | public function __set($name, $value) 93 | { 94 | $this->config->$name = $value; 95 | } 96 | 97 | /** 98 | * Handle the dynamic retrieval of attachment options. 99 | * Style options will be converted into a php stcClass. 100 | * 101 | * @param string $optionName 102 | * 103 | * @return mixed 104 | */ 105 | public function __get($optionName) 106 | { 107 | return $this->config->$optionName; 108 | } 109 | 110 | /** 111 | * Mutator method for the uploadedFile property. 112 | * Accepts the following inputs: 113 | * - An absolute string url (for fetching remote files). 114 | * - An array (data parsed from the $_FILES array), 115 | * - A Symfony uploaded file object. 116 | * 117 | * @param mixed $uploadedFile 118 | */ 119 | public function setUploadedFile($uploadedFile) 120 | { 121 | if (!$this->keep_old_files) { 122 | $this->clear(); 123 | } 124 | 125 | if ($uploadedFile == STAPLER_NULL) { 126 | $this->clearAttributes(); 127 | 128 | return; 129 | } 130 | 131 | $this->uploadedFile = FileFactory::create($uploadedFile); 132 | $this->instanceWrite('file_name', $this->uploadedFile->getFilename()); 133 | $this->instanceWrite('file_size', $this->uploadedFile->getSize()); 134 | $this->instanceWrite('content_type', $this->uploadedFile->getMimeType()); 135 | $this->instanceWrite('updated_at', new DateTime); 136 | $this->queueAllForWrite(); 137 | } 138 | 139 | /** 140 | * Accessor method for the uploadedFile property. 141 | * 142 | * @return \Codesleeve\Stapler\Interfaces\File 143 | */ 144 | public function getUploadedFile() 145 | { 146 | return $this->uploadedFile; 147 | } 148 | 149 | /** 150 | * Mutator method for the interpolator property. 151 | * 152 | * @param InterpolatorInterface $interpolator 153 | */ 154 | public function setInterpolator(InterpolatorInterface $interpolator) 155 | { 156 | $this->interpolator = $interpolator; 157 | } 158 | 159 | /** 160 | * Accessor method for the interpolator property. 161 | * 162 | * @return InterpolatorInterface 163 | */ 164 | public function getInterpolator() 165 | { 166 | return $this->interpolator; 167 | } 168 | 169 | /** 170 | * Mutator method for the resizer property. 171 | * 172 | * @param ResizerInterface $resizer 173 | */ 174 | public function setResizer(ResizerInterface $resizer) 175 | { 176 | $this->resizer = $resizer; 177 | } 178 | 179 | /** 180 | * Accessor method for the uploadedFile property. 181 | * 182 | * @return Resizer 183 | */ 184 | public function getResizer() 185 | { 186 | return $this->resizer; 187 | } 188 | 189 | /** 190 | * Mutator method for the storageDriver property. 191 | * 192 | * @param StorageInterface $storageDriver 193 | */ 194 | public function setStorageDriver(StorageInterface $storageDriver) 195 | { 196 | $this->storageDriver = $storageDriver; 197 | } 198 | 199 | /** 200 | * Accessor method for the storageDriver property. 201 | * 202 | * @return StorageInterface 203 | */ 204 | public function getStorageDriver() 205 | { 206 | return $this->storageDriver; 207 | } 208 | 209 | /** 210 | * Mutator method for the instance property. 211 | * This provides a mechanism for the attachment to access properties of the 212 | * corresponding model instance it's attached to. 213 | * 214 | * @param StaplerableInterface $instance 215 | */ 216 | public function setInstance(StaplerableInterface $instance) 217 | { 218 | $this->instance = $instance; 219 | } 220 | 221 | /** 222 | * Accessor method for the underlying 223 | * instance (model) object this attachment 224 | * is defined on. 225 | * 226 | * @return StaplerableInterface 227 | */ 228 | public function getInstance() 229 | { 230 | return $this->instance; 231 | } 232 | 233 | /** 234 | * Mutator method for the config property. 235 | * 236 | * @param AttachmentConfig $config 237 | */ 238 | public function setConfig(AttachmentConfig $config) 239 | { 240 | $this->config = $config; 241 | } 242 | 243 | /** 244 | * Accessor method for the Config property. 245 | * 246 | * @return array 247 | */ 248 | public function getConfig() 249 | { 250 | return $this->config; 251 | } 252 | 253 | /** 254 | * Accessor method for the QueuedForDeletion property. 255 | * 256 | * @return array 257 | */ 258 | public function getQueuedForDeletion() 259 | { 260 | return $this->queuedForDeletion; 261 | } 262 | 263 | /** 264 | * Mutator method for the QueuedForDeletion property. 265 | * 266 | * @param array $array 267 | */ 268 | public function setQueuedForDeletion($array) 269 | { 270 | $this->queuedForDeletion = $array; 271 | } 272 | 273 | /** 274 | * Handle dynamic method calls on the attachment. 275 | * This allows us to call methods on the underlying 276 | * storage driver directly via the attachment. 277 | * 278 | * @param string $method 279 | * @param array $parameters 280 | * 281 | * @return mixed 282 | */ 283 | public function __call($method, $parameters) 284 | { 285 | $callable = ['remove', 'move']; 286 | 287 | if (in_array($method, $callable)) { 288 | return call_user_func_array([$this->storageDriver, $method], $parameters); 289 | } 290 | } 291 | 292 | /** 293 | * Generates the url to an uploaded file (or a resized version of it). 294 | * 295 | * @param string $styleName 296 | * 297 | * @return string 298 | */ 299 | public function url($styleName = '') 300 | { 301 | if ($this->originalFilename()) { 302 | return $this->storageDriver->url($styleName, $this); 303 | } 304 | 305 | return $this->defaultUrl($styleName); 306 | } 307 | 308 | /** 309 | * Generates the file system path to an uploaded file (or a resized version of it). 310 | * This is used for saving files, etc. 311 | * 312 | * @param string $styleName 313 | * 314 | * @return string 315 | */ 316 | public function path($styleName = '') 317 | { 318 | if ($this->originalFilename()) { 319 | return $this->storageDriver->path($styleName, $this); 320 | } 321 | 322 | return $this->defaultPath($styleName); 323 | } 324 | 325 | /** 326 | * Returns the creation time of the file as originally assigned to this attachment's model. 327 | * Lives in the _created_at attribute of the model. 328 | * This attribute may conditionally exist on the model, it is not one of the four required fields. 329 | * 330 | * @return string 331 | */ 332 | public function createdAt() 333 | { 334 | return $this->instance->getAttribute("{$this->name}_created_at"); 335 | } 336 | 337 | /** 338 | * Returns the last modified time of the file as originally assigned to this attachment's model. 339 | * Lives in the _updated_at attribute of the model. 340 | * 341 | * @return string 342 | */ 343 | public function updatedAt() 344 | { 345 | return $this->instance->getAttribute("{$this->name}_updated_at"); 346 | } 347 | 348 | /** 349 | * Returns the content type of the file as originally assigned to this attachment's model. 350 | * Lives in the _content_type attribute of the model. 351 | * 352 | * @return string 353 | */ 354 | public function contentType() 355 | { 356 | return $this->instance->getAttribute("{$this->name}_content_type"); 357 | } 358 | 359 | /** 360 | * Returns the size of the file as originally assigned to this attachment's model. 361 | * Lives in the _file_size attribute of the model. 362 | * 363 | * @return int 364 | */ 365 | public function size() 366 | { 367 | return $this->instance->getAttribute("{$this->name}_file_size"); 368 | } 369 | 370 | /** 371 | * Returns the name of the file as originally assigned to this attachment's model. 372 | * Lives in the _file_name attribute of the model. 373 | * 374 | * @return string 375 | */ 376 | public function originalFilename() 377 | { 378 | return $this->instance->getAttribute("{$this->name}_file_name"); 379 | } 380 | 381 | /** 382 | * Returns the class type of the attachment's underlying 383 | * model instance. 384 | * 385 | * @return string 386 | */ 387 | public function getInstanceClass() 388 | { 389 | return get_class($this->instance); 390 | } 391 | 392 | /** 393 | * Rebuilds the images for this attachment. 394 | */ 395 | public function reprocess() 396 | { 397 | if (!$this->originalFilename()) { 398 | return; 399 | } 400 | 401 | foreach ($this->styles as $style) { 402 | $fileLocation = $this->storage == 'filesystem' ? $this->path('original') : $this->url('original'); 403 | $file = FileFactory::create($fileLocation); 404 | 405 | if ($style->dimensions && $file->isImage()) { 406 | $file = $this->resizer->resize($file, $style); 407 | } else { 408 | $file = $file->getRealPath(); 409 | } 410 | 411 | $filePath = $this->path($style->name); 412 | $this->move($file, $filePath); 413 | } 414 | } 415 | 416 | /** 417 | * Process the write queue. 418 | * 419 | * @param StaplerableInterface $instance 420 | */ 421 | public function afterSave(StaplerableInterface $instance) 422 | { 423 | $this->instance = $instance; 424 | $this->save(); 425 | } 426 | 427 | /** 428 | * Queue up this attachments files for deletion. 429 | * 430 | * @param StaplerableInterface $instance 431 | */ 432 | public function beforeDelete(StaplerableInterface $instance) 433 | { 434 | $this->instance = $instance; 435 | 436 | if (!$this->preserve_files) { 437 | $this->clear(); 438 | } 439 | } 440 | 441 | /** 442 | * Process the delete queue. 443 | * 444 | * @param StaplerableInterface $instance 445 | */ 446 | public function afterDelete(StaplerableInterface $instance) 447 | { 448 | $this->instance = $instance; 449 | $this->flushDeletes(); 450 | } 451 | 452 | /** 453 | * Removes all uploaded files (from storage) for this attachment. 454 | * This method does not clear out attachment attributes on the model instance. 455 | * 456 | * @param array $stylesToClear 457 | */ 458 | public function destroy(array $stylesToClear = []) 459 | { 460 | $this->clear($stylesToClear); 461 | $this->flushDeletes(); 462 | } 463 | 464 | /** 465 | * Queues up all or some of this attachments uploaded files/images for deletion. 466 | * 467 | * @param array $stylesToClear 468 | */ 469 | public function clear(array $stylesToClear = []) 470 | { 471 | if ($stylesToClear) { 472 | $this->queueSomeForDeletion($stylesToClear); 473 | } else { 474 | $this->queueAllForDeletion(); 475 | } 476 | } 477 | 478 | /** 479 | * Flushes the queuedForDeletion and queuedForWrite arrays. 480 | */ 481 | public function save() 482 | { 483 | $this->flushDeletes(); 484 | $this->flushWrites(); 485 | } 486 | 487 | /** 488 | * Set an attachment attribute on the underlying model instance. 489 | * 490 | * @param string $property 491 | * @param mixed $value 492 | */ 493 | public function instanceWrite($property, $value) 494 | { 495 | $fieldName = "{$this->name}_{$property}"; 496 | $this->instance->setAttribute($fieldName, $value); 497 | } 498 | 499 | /** 500 | * Clear (set to null) all attachment related model 501 | * attributes. 502 | */ 503 | public function clearAttributes() 504 | { 505 | $this->instanceWrite('file_name', null); 506 | $this->instanceWrite('file_size', null); 507 | $this->instanceWrite('content_type', null); 508 | $this->instanceWrite('updated_at', null); 509 | } 510 | 511 | /** 512 | * Return a JSON representation of this class. 513 | * 514 | * @return array 515 | */ 516 | public function jsonSerialize() 517 | { 518 | $data = []; 519 | 520 | foreach ($this->styles as $style) { 521 | $data[$style->name] = [ 522 | 'path' => $this->path($style->name), 523 | 'url' => $this->url($style->name) 524 | ]; 525 | } 526 | 527 | return $data; 528 | } 529 | 530 | /** 531 | * Process the queuedForWrite que. 532 | */ 533 | protected function flushWrites() 534 | { 535 | foreach ($this->queuedForWrite as $style) { 536 | if ($style->dimensions && $this->uploadedFile->isImage()) { 537 | $file = $this->resizer->resize($this->uploadedFile, $style); 538 | } else { 539 | $file = $this->uploadedFile->getRealPath(); 540 | } 541 | 542 | $filePath = $this->path($style->name); 543 | $this->move($file, $filePath); 544 | } 545 | 546 | $this->queuedForWrite = []; 547 | } 548 | 549 | /** 550 | * Process the queuedForDeletion que. 551 | */ 552 | protected function flushDeletes() 553 | { 554 | $this->remove($this->queuedForDeletion); 555 | $this->queuedForDeletion = []; 556 | } 557 | 558 | /** 559 | * Fill the queuedForWrite que with all of this attachment's styles. 560 | */ 561 | protected function queueAllForWrite() 562 | { 563 | $this->queuedForWrite = $this->styles; 564 | } 565 | 566 | /** 567 | * Add a subset (filtered via style) of the uploaded files for this attachment 568 | * to the queuedForDeletion queue. 569 | * 570 | * @param array $stylesToClear 571 | */ 572 | protected function queueSomeForDeletion(array $stylesToClear) 573 | { 574 | $filePaths = array_map(function ($styleToClear) { 575 | return $this->path($styleToClear); 576 | }, $stylesToClear); 577 | 578 | $this->queuedForDeletion = array_merge($this->queuedForDeletion, $filePaths); 579 | } 580 | 581 | /** 582 | * Add all uploaded files (across all image styles) to the queuedForDeletion queue. 583 | */ 584 | protected function queueAllForDeletion() 585 | { 586 | if (!$this->originalFilename()) { 587 | return; 588 | } 589 | 590 | $filePaths = array_map(function ($style) { 591 | return $this->path($style->name); 592 | }, $this->styles); 593 | 594 | $this->queuedForDeletion = array_merge($this->queuedForDeletion, $filePaths); 595 | } 596 | 597 | /** 598 | * Generates the default url if no file attachment is present. 599 | * 600 | * @param string $styleName 601 | * 602 | * @return string 603 | */ 604 | protected function defaultUrl($styleName = '') 605 | { 606 | if ($url = $this->default_url) { 607 | return $this->getInterpolator()->interpolate($url, $this, $styleName); 608 | } 609 | 610 | return ''; 611 | } 612 | 613 | /** 614 | * Generates the default path if no file attachment is present. 615 | * 616 | * @param string $styleName 617 | * 618 | * @return string 619 | */ 620 | protected function defaultPath($styleName = '') 621 | { 622 | return $this->public_path.$this->defaultUrl($styleName); 623 | } 624 | } 625 | -------------------------------------------------------------------------------- /src/AttachmentConfig.php: -------------------------------------------------------------------------------- 1 | name = $name; 43 | $this->options = $options; 44 | $this->styles = $this->buildStyleObjects($options['styles']); 45 | } 46 | 47 | /** 48 | * Handle the dynamic setting of attachment options. 49 | * 50 | * @param string $name 51 | * @param mixed $value 52 | */ 53 | public function __set($name, $value) 54 | { 55 | $this->options[$name] = $value; 56 | } 57 | 58 | /** 59 | * Handle the dynamic retrieval of attachment options. 60 | * Style options will be converted into a php stcClass. 61 | * 62 | * @param string $optionName 63 | * 64 | * @return mixed 65 | */ 66 | public function __get($optionName) 67 | { 68 | if (array_key_exists($optionName, $this->options)) { 69 | if ($optionName == 'styles') { 70 | return $this->styles; 71 | } 72 | 73 | return $this->options[$optionName]; 74 | } 75 | 76 | return; 77 | } 78 | 79 | /** 80 | * Convert the styles array into an array of Style objects. 81 | * Both array keys and array values will be converted to object properties. 82 | * 83 | * @param mixed $styles 84 | * 85 | * @return array 86 | */ 87 | protected function buildStyleObjects($styles) 88 | { 89 | $config = Stapler::getConfigInstance(); 90 | $className = $config->get('bindings.style'); 91 | $styleObjects = []; 92 | 93 | foreach ($styles as $styleName => $styleValue) { 94 | $styleObjects[] = new $className($styleName, $styleValue); 95 | } 96 | 97 | return $styleObjects; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Config/NativeConfig.php: -------------------------------------------------------------------------------- 1 | [ 17 | 'public_path' => '', 18 | 'base_path' => '', 19 | 'storage' => 'filesystem', 20 | 'image_processing_library' => 'Imagine\Gd\Imagine', 21 | 'default_url' => '/:attachment/:style/missing.png', 22 | 'default_style' => 'original', 23 | 'styles' => [], 24 | 'keep_old_files' => false, 25 | 'preserve_files' => false, 26 | ], 27 | 'filesystem' => [ 28 | 'url' => '/system/:class/:attachment/:id_partition/:style/:filename', 29 | 'path' => ':app_root/public:url', 30 | 'override_file_permissions' => null, 31 | ], 32 | 's3' => [ 33 | 's3_client_config' => [ 34 | 'key' => '', 35 | 'secret' => '', 36 | 'region' => '', 37 | 'scheme' => 'http', 38 | ], 39 | 's3_object_config' => [ 40 | 'Bucket' => '', 41 | 'ACL' => 'public-read', 42 | ], 43 | 'path' => ':attachment/:id/:style/:filename', 44 | ], 45 | 'bindings' => [ 46 | 'attachment' => '\Codesleeve\Stapler\Attachment', 47 | 'interpolator' => '\Codesleeve\Stapler\Interpolator', 48 | 'resizer' => '\Codesleeve\Stapler\File\Image\Resizer', 49 | 'style' => '\Codesleeve\Stapler\Style', 50 | 'validator' => '\Codesleeve\Stapler\Validator', 51 | ] 52 | ]; 53 | 54 | /** 55 | * Constructor method. 56 | * 57 | * @param array $items 58 | */ 59 | public function __construct(array $items = null) 60 | { 61 | if ($items) { 62 | $this->items = $items; 63 | } 64 | } 65 | 66 | /** 67 | * Retrieve a configuration value. 68 | * 69 | * @param $name 70 | * 71 | * @return mixed 72 | */ 73 | public function get($name) 74 | { 75 | list($group, $item) = array_pad(explode('.', $name), 2, null); 76 | 77 | if ($item) { 78 | return $this->loadItemFromFile($group, $item); 79 | } 80 | 81 | return $this->loadAllFromFile($group); 82 | } 83 | 84 | /** 85 | * Set a configuration value. 86 | * 87 | * @param $name 88 | * @param $value 89 | */ 90 | public function set($name, $value) 91 | { 92 | list($group, $item) = array_pad(explode('.', $name), 2, null); 93 | 94 | if ($item) { 95 | $this->items[$group][$item] = $value; 96 | } else { 97 | $this->items[$group] = $value; 98 | } 99 | } 100 | 101 | /** 102 | * Load a specific configuration item from a specific 103 | * configuration group. 104 | * 105 | * @param string $group 106 | * @param string $item 107 | */ 108 | protected function loadItemFromFile($group, $item) 109 | { 110 | if (array_key_exists($group, $this->items) && array_key_exists($item, $this->items[$group])) { 111 | return $this->items[$group][$item]; 112 | } 113 | } 114 | 115 | /** 116 | * Load all configuration items from a specific 117 | * configuration group. 118 | * 119 | * @param string $group 120 | */ 121 | protected function loadAllFromFile($group) 122 | { 123 | if (array_key_exists($group, $this->items)) { 124 | return $this->items[$group]; 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/Exceptions/FileException.php: -------------------------------------------------------------------------------- 1 | validateOptions($options); 23 | list($config, $interpolator, $resizer) = static::buildDependencies($name, $options); 24 | 25 | $className = Stapler::getConfigInstance()->get('bindings.attachment'); 26 | $attachment = new $className($config, $interpolator, $resizer); 27 | 28 | $storageDriver = StorageFactory::create($attachment); 29 | $attachment->setStorageDriver($storageDriver); 30 | 31 | return $attachment; 32 | } 33 | 34 | /** 35 | * Build out the dependencies required to create 36 | * a new attachment object. 37 | * 38 | * @param string $name 39 | * @param array $options 40 | * 41 | * @return array 42 | */ 43 | protected static function buildDependencies($name, array $options) 44 | { 45 | return [ 46 | new AttachmentConfig($name, $options), 47 | Stapler::getInterpolatorInstance(), 48 | Stapler::getResizerInstance($options['image_processing_library']), 49 | ]; 50 | } 51 | 52 | /** 53 | * Merge configuration options. 54 | * Here we'll merge user defined options with the stapler defaults in a cascading manner. 55 | * We start with overall stapler options. Next we merge in storage driver specific options. 56 | * Finally we'll merge in attachment specific options on top of that. 57 | * 58 | * @param array $options 59 | * 60 | * @return array 61 | */ 62 | protected static function mergeOptions(array $options) 63 | { 64 | $config = Stapler::getConfigInstance(); 65 | $defaultOptions = $config->get('stapler'); 66 | $options = array_merge($defaultOptions, (array) $options); 67 | $storage = $options['storage']; 68 | $options = array_replace_recursive($config->get($storage), $options); 69 | $options['styles'] = array_merge((array) $options['styles'], ['original' => '']); 70 | 71 | return $options; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Factories/File.php: -------------------------------------------------------------------------------- 1 | validate(); 62 | 63 | return $staplerFile; 64 | } 65 | 66 | /** 67 | * Compose a \Codesleeve\Stapler\File\UploadedFile object from a 68 | * data uri. 69 | * 70 | * @param string $file 71 | * @return \Codesleeve\Stapler\File\File 72 | */ 73 | protected static function createFromDataURI($file) 74 | { 75 | $fp = @fopen($file, 'r'); 76 | 77 | if (!$fp) { 78 | throw new \Codesleeve\Stapler\Exceptions\FileException('Invalid data URI'); 79 | } 80 | 81 | $meta = stream_get_meta_data($fp); 82 | $extension = static::getMimeTypeExtensionGuesserInstance()->guess($meta['mediatype']); 83 | $filePath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . md5($meta['uri']) . '.' . $extension; 84 | 85 | file_put_contents($filePath, stream_get_contents($fp)); 86 | 87 | return new StaplerFile($filePath); 88 | } 89 | 90 | /** 91 | * Build a Codesleeve\Stapler\File\File object from the 92 | * raw php $_FILES array date. We assume here that the $_FILES array 93 | * has been formated using the Stapler::arrangeFiles utility method. 94 | * 95 | * @param array $file 96 | * @param bool $testing 97 | * 98 | * @return \Codesleeve\Stapler\File\File 99 | */ 100 | protected static function createFromArray(array $file, $testing) 101 | { 102 | $file = new SymfonyUploadedFile($file['tmp_name'], $file['name'], $file['type'], $file['size'], $file['error'], $testing); 103 | 104 | return static::createFromObject($file); 105 | } 106 | 107 | /** 108 | * Fetch a remote file using a string URL and convert it into 109 | * an instance of Codesleeve\Stapler\File\File. 110 | * 111 | * @param string $file 112 | * 113 | * @return \Codesleeve\Stapler\File\File 114 | */ 115 | protected static function createFromUrl($file) 116 | { 117 | $ch = curl_init($file); 118 | curl_setopt($ch, CURLOPT_HEADER, 0); 119 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 120 | curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); 121 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); 122 | $rawFile = curl_exec($ch); 123 | curl_close($ch); 124 | 125 | // Remove the query string and hash if they exist 126 | $file = preg_replace('/[&#\?].*/', '', $file); 127 | 128 | // Get the original name of the file 129 | $pathinfo = pathinfo($file); 130 | $name = $pathinfo['basename']; 131 | $extension = isset($pathinfo['extension']) ? '.'.$pathinfo['extension'] : ''; 132 | 133 | // Create a temporary file with a unique name. 134 | $tempFile = tempnam(sys_get_temp_dir(), 'stapler-'); 135 | 136 | if ($extension) { 137 | $filePath = $tempFile."{$extension}"; 138 | } else { 139 | // Since we don't have an extension for the file, we'll have to go ahead and write 140 | // the contents of the rawfile to disk (using the tempFile path) in order to use 141 | // symfony's mime type guesser to generate an extension for the file. 142 | file_put_contents($tempFile, $rawFile); 143 | $mimeType = MimeTypeGuesser::getInstance()->guess($tempFile); 144 | $extension = static::getMimeTypeExtensionGuesserInstance()->guess($mimeType); 145 | 146 | $filePath = $tempFile.'.'.$extension; 147 | } 148 | 149 | file_put_contents($filePath, $rawFile); 150 | unlink($tempFile); 151 | 152 | return new StaplerFile($filePath); 153 | } 154 | 155 | /** 156 | * Fetch a local file using a string location and convert it into 157 | * an instance of \Codesleeve\Stapler\File\File. 158 | * 159 | * @param string $file 160 | * 161 | * @return \Codesleeve\Stapler\File\File 162 | */ 163 | protected static function createFromString($file) 164 | { 165 | return new StaplerFile($file, pathinfo($file)['basename']); 166 | } 167 | 168 | /** 169 | * Return an instance of the Symfony MIME type extension guesser. 170 | * 171 | * @return \Symfony\Component\HttpFoundation\File\MimeType\MimeTypeExtensionGuesserInterface 172 | */ 173 | public static function getMimeTypeExtensionGuesserInstance() 174 | { 175 | if (!static::$mimeTypeExtensionGuesser) { 176 | static::$mimeTypeExtensionGuesser = new MimeTypeExtensionGuesser(); 177 | } 178 | 179 | return static::$mimeTypeExtensionGuesser; 180 | } 181 | 182 | /** 183 | * Set the configuration object instance. 184 | * 185 | * @param ConfigInterface $config 186 | */ 187 | public static function setConfigInstance(ConfigInterface $config) 188 | { 189 | static::$config = $config; 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/Factories/Storage.php: -------------------------------------------------------------------------------- 1 | storage) { 22 | case 'filesystem': 23 | return new Filesystem($attachment); 24 | break; 25 | 26 | case 's3': 27 | $s3Client = Stapler::getS3ClientInstance($attachment); 28 | 29 | return new S3($attachment, $s3Client); 30 | break; 31 | 32 | default: 33 | return new Filesystem($attachment); 34 | break; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/File/File.php: -------------------------------------------------------------------------------- 1 | 'image/bmp', 18 | 'gif' => 'image/gif', 19 | 'jpeg' => ['image/jpeg', 'image/pjpeg'], 20 | 'jpg' => ['image/jpeg', 'image/pjpeg'], 21 | 'jpe' => ['image/jpeg', 'image/pjpeg'], 22 | 'png' => 'image/png', 23 | 'tiff' => 'image/tiff', 24 | 'tif' => 'image/tiff', 25 | ]; 26 | 27 | /** 28 | * Method for determining whether the uploaded file is 29 | * an image type. 30 | * 31 | * @return bool 32 | */ 33 | public function isImage() 34 | { 35 | $mime = $this->getMimeType(); 36 | 37 | // The $imageMimes property contains an array of file extensions and 38 | // their associated MIME types. We will loop through them and look for 39 | // the MIME type of the current SymfonyUploadedFile. 40 | foreach ($this->imageMimes as $imageMime) { 41 | if (in_array($mime, (array) $imageMime)) { 42 | return true; 43 | } 44 | } 45 | 46 | return false; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/File/Image/Resizer.php: -------------------------------------------------------------------------------- 1 | imagine = $imagine; 30 | } 31 | 32 | /** 33 | * Resize an image using the computed settings. 34 | * 35 | * @param FileInterface $file 36 | * @param StyleInterface $style 37 | * 38 | * @return string 39 | */ 40 | public function resize(FileInterface $file, StyleInterface $style) 41 | { 42 | $filePath = $this->randomFilePath($file->getFilename()); 43 | list($width, $height, $option) = $this->parseStyleDimensions($style); 44 | $method = 'resize'.ucfirst($option); 45 | 46 | if ($method == 'resizeCustom') { 47 | $this->resizeCustom($file, $style->dimensions) 48 | ->save($filePath, $style->convertOptions); 49 | 50 | return $filePath; 51 | } 52 | 53 | $image = $this->imagine->open($file->getRealPath()); 54 | 55 | if ($style->autoOrient) { 56 | $image = $this->autoOrient($file->getRealPath(), $image); 57 | } 58 | 59 | $this->$method($image, $width, $height) 60 | ->save($filePath, $style->convertOptions); 61 | 62 | return $filePath; 63 | } 64 | 65 | /** 66 | * Accessor method for the $imagine property. 67 | * 68 | * @param ImagineInterface $imagine 69 | */ 70 | public function setImagine(ImagineInterface $imagine) 71 | { 72 | $this->imagine = $imagine; 73 | } 74 | 75 | /** 76 | * parseStyleDimensions method. 77 | * 78 | * Parse the given style dimensions to extract out the file processing options, 79 | * perform any necessary image resizing for a given style. 80 | * 81 | * @param StyleInterface $style 82 | * 83 | * @return array 84 | */ 85 | protected function parseStyleDimensions(StyleInterface $style) 86 | { 87 | if (is_callable($style->dimensions)) { 88 | return [null, null, 'custom']; 89 | } 90 | 91 | if (strpos($style->dimensions, 'x') === false) { 92 | // Width given, height automagically selected to preserve aspect ratio (landscape). 93 | $width = $style->dimensions; 94 | 95 | return [$width, null, 'landscape']; 96 | } 97 | 98 | $dimensions = explode('x', $style->dimensions); 99 | $width = $dimensions[0]; 100 | $height = $dimensions[1]; 101 | 102 | if (empty($width)) { 103 | // Height given, width automagically selected to preserve aspect ratio (portrait). 104 | return [null, $height, 'portrait']; 105 | } 106 | 107 | $resizingOption = substr($height, -1, 1); 108 | 109 | if ($resizingOption == '#') { 110 | // Resize, then crop. 111 | $height = rtrim($height, '#'); 112 | 113 | return [$width, $height, 'crop']; 114 | } 115 | 116 | if ($resizingOption == '!') { 117 | // Resize by exact width/height (does not preserve aspect ratio). 118 | $height = rtrim($height, '!'); 119 | 120 | return [$width, $height, 'exact']; 121 | } 122 | 123 | // Let the script decide the best way to resize. 124 | return [$width, $height, 'auto']; 125 | } 126 | 127 | /** 128 | * Resize an image as closely as possible to a given 129 | * width and height while still maintaining aspect ratio. 130 | * This method is really just a proxy to other resize methods:. 131 | * 132 | * If the current image is wider than it is tall, we'll resize landscape. 133 | * If the current image is taller than it is wide, we'll resize portrait. 134 | * If the image is as tall as it is wide (it's a squarey) then we'll 135 | * apply the same process using the new dimensions (we'll resize exact if 136 | * the new dimensions are both equal since at this point we'll have a square 137 | * image being resized to a square). 138 | * 139 | * @param ImageInterface $image 140 | * @param string $width - The image's new width. 141 | * @param string $height - The image's new height. 142 | * 143 | * @return ImageInterface 144 | */ 145 | protected function resizeAuto(ImageInterface $image, $width, $height) 146 | { 147 | $size = $image->getSize(); 148 | $originalWidth = $size->getWidth(); 149 | $originalHeight = $size->getHeight(); 150 | 151 | if ($originalHeight < $originalWidth) { 152 | return $this->resizeLandscape($image, $width, $height); 153 | } 154 | 155 | if ($originalHeight > $originalWidth) { 156 | return $this->resizePortrait($image, $width, $height); 157 | } 158 | 159 | if ($height < $width) { 160 | return $this->resizeLandscape($image, $width, $height); 161 | } 162 | 163 | if ($height > $width) { 164 | return $this->resizePortrait($image, $width, $height); 165 | } 166 | 167 | return $this->resizeExact($image, $width, $height); 168 | } 169 | 170 | /** 171 | * Resize an image as a landscape (width fixed). 172 | * 173 | * @param ImageInterface $image 174 | * @param string $width - The image's new width. 175 | * @param string $height - The image's new height. 176 | * 177 | * @return ImageInterface 178 | */ 179 | protected function resizeLandscape(ImageInterface $image, $width, $height) 180 | { 181 | $optimalHeight = $this->getSizeByFixedWidth($image, $width); 182 | $dimensions = $image->getSize() 183 | ->widen($width) 184 | ->heighten($optimalHeight); 185 | 186 | $image = $image->resize($dimensions); 187 | 188 | return $image; 189 | } 190 | 191 | /** 192 | * Resize an image as a portrait (height fixed). 193 | * 194 | * @param ImageInterface $image 195 | * @param string $width - The image's new width. 196 | * @param string $height - The image's new height. 197 | * 198 | * @return ImageInterface 199 | */ 200 | protected function resizePortrait(ImageInterface $image, $width, $height) 201 | { 202 | $optimalWidth = $this->getSizeByFixedHeight($image, $height); 203 | $dimensions = $image->getSize() 204 | ->heighten($height) 205 | ->widen($optimalWidth); 206 | 207 | $image = $image->resize($dimensions); 208 | 209 | return $image; 210 | } 211 | 212 | /** 213 | * Resize an image and then center crop it. 214 | * 215 | * @param ImageInterface $image 216 | * @param string $width - The image's new width. 217 | * @param string $height - The image's new height. 218 | * 219 | * @return ImageInterface 220 | */ 221 | protected function resizeCrop(ImageInterface $image, $width, $height) 222 | { 223 | list($optimalWidth, $optimalHeight) = $this->getOptimalCrop($image->getSize(), $width, $height); 224 | 225 | // Find center - this will be used for the crop 226 | $centerX = ($optimalWidth / 2) - ($width / 2); 227 | $centerY = ($optimalHeight / 2) - ($height / 2); 228 | 229 | return $image->resize(new Box($optimalWidth, $optimalHeight)) 230 | ->crop(new Point($centerX, $centerY), new Box($width, $height)); 231 | } 232 | 233 | /** 234 | * Resize an image to an exact width and height. 235 | * 236 | * @param ImageInterface $image 237 | * @param string $width - The image's new width. 238 | * @param string $height - The image's new height. 239 | * 240 | * @return ImageInterface 241 | */ 242 | protected function resizeExact(ImageInterface $image, $width, $height) 243 | { 244 | return $image->resize(new Box($width, $height)); 245 | } 246 | 247 | /** 248 | * Resize an image using a user defined callback. 249 | * 250 | * @param FileInterface $file 251 | * @param $callable 252 | * 253 | * @return ImageInterface 254 | */ 255 | protected function resizeCustom(FileInterface $file, callable $callable) 256 | { 257 | return call_user_func_array($callable, [$file, $this->imagine]); 258 | } 259 | 260 | /** 261 | * Returns the width based on the new image height. 262 | * 263 | * @param ImageInterface $image 264 | * @param int $newHeight - The image's new height. 265 | * 266 | * @return int 267 | */ 268 | private function getSizeByFixedHeight(ImageInterface $image, $newHeight) 269 | { 270 | $box = $image->getSize(); 271 | $ratio = $box->getWidth() / $box->getHeight(); 272 | $newWidth = $newHeight * $ratio; 273 | 274 | return $newWidth; 275 | } 276 | 277 | /** 278 | * Returns the height based on the new image width. 279 | * 280 | * @param ImageInterface $image 281 | * @param int $newWidth - The image's new width. 282 | * 283 | * @return int 284 | */ 285 | private function getSizeByFixedWidth(ImageInterface $image, $newWidth) 286 | { 287 | $box = $image->getSize(); 288 | $ratio = $box->getHeight() / $box->getWidth(); 289 | $newHeight = $newWidth * $ratio; 290 | 291 | return $newHeight; 292 | } 293 | 294 | /** 295 | * Attempts to find the best way to crop. 296 | * Takes into account the image being a portrait or landscape. 297 | * 298 | * @param Box $size - The image's current size. 299 | * @param string $width - The image's new width. 300 | * @param string $height - The image's new height. 301 | * 302 | * @return array 303 | */ 304 | protected function getOptimalCrop(Box $size, $width, $height) 305 | { 306 | $heightRatio = $size->getHeight() / $height; 307 | $widthRatio = $size->getWidth() / $width; 308 | 309 | if ($heightRatio < $widthRatio) { 310 | $optimalRatio = $heightRatio; 311 | } else { 312 | $optimalRatio = $widthRatio; 313 | } 314 | 315 | $optimalHeight = round($size->getHeight() / $optimalRatio, 2); 316 | $optimalWidth = round($size->getWidth() / $optimalRatio, 2); 317 | 318 | return [$optimalWidth, $optimalHeight]; 319 | } 320 | 321 | /** 322 | * Re-orient an image using its embedded Exif profile orientation: 323 | * 1. Attempt to read the embedded exif data inside the image to determine it's orientation. 324 | * if there is no exif data (i.e an exeption is thrown when trying to read it) then we'll 325 | * just return the image as is. 326 | * 2. If there is exif data, we'll rotate and flip the image accordingly to re-orient it. 327 | * 3. Finally, we'll strip the exif data from the image so that there can be no attempt to 'correct' it again. 328 | * 329 | * @param string $path 330 | * @param ImageInterface $image 331 | * 332 | * @return ImageInterface $image 333 | */ 334 | protected function autoOrient($path, ImageInterface $image) 335 | { 336 | if (function_exists('exif_read_data')) { 337 | try { 338 | $exif = exif_read_data($path); 339 | } catch (\ErrorException $e) { 340 | return $image; 341 | } 342 | 343 | if (isset($exif['Orientation'])) { 344 | switch ($exif['Orientation']) { 345 | case 2: 346 | $image->flipHorizontally(); 347 | break; 348 | case 3: 349 | $image->rotate(180); 350 | break; 351 | case 4: 352 | $image->flipVertically(); 353 | break; 354 | case 5: 355 | $image->flipVertically() 356 | ->rotate(90); 357 | break; 358 | case 6: 359 | $image->rotate(90); 360 | break; 361 | case 7: 362 | $image->flipHorizontally() 363 | ->rotate(90); 364 | break; 365 | case 8: 366 | $image->rotate(-90); 367 | break; 368 | } 369 | } 370 | 371 | return $image->strip(); 372 | } else { 373 | return $image; 374 | } 375 | } 376 | 377 | /** 378 | * Given the name of a file, generate temp a path 379 | * with a radomized filename. 380 | * 381 | * @param string $filename 382 | * @return string 383 | */ 384 | protected function randomFilePath($filename) 385 | { 386 | $chars = 'abcdefghijklmnopqrstuvwxyz0123456789'; 387 | $filePath = sys_get_temp_dir() . '/stapler.'; 388 | 389 | for ($i = 0; $i < 10; $i++) { 390 | $filePath .= $chars[mt_rand(0, 35)]; 391 | } 392 | 393 | $filePath .= '_' . $filename; 394 | 395 | return $filePath; 396 | } 397 | } 398 | -------------------------------------------------------------------------------- /src/File/UploadedFile.php: -------------------------------------------------------------------------------- 1 | 'image/bmp', 27 | 'gif' => 'image/gif', 28 | 'jpeg' => ['image/jpeg', 'image/pjpeg'], 29 | 'jpg' => ['image/jpeg', 'image/pjpeg'], 30 | 'jpe' => ['image/jpeg', 'image/pjpeg'], 31 | 'png' => 'image/png', 32 | 'tiff' => 'image/tiff', 33 | 'tif' => 'image/tiff', 34 | ]; 35 | 36 | /** 37 | * Constructor method. 38 | * 39 | * @param SymfonyUploadedFile $uploadedFile 40 | */ 41 | public function __construct(SymfonyUploadedFile $uploadedFile) 42 | { 43 | $this->uploadedFile = $uploadedFile; 44 | } 45 | 46 | /** 47 | * Handle dynamic method calls on this class. 48 | * This method allows this class to act as a 'composite' object 49 | * by delegating method calls to the underlying SymfonyUploadedFile object. 50 | * 51 | * @param string $method 52 | * @param array $parameters 53 | * 54 | * @return mixed 55 | */ 56 | public function __call($method, array $parameters) 57 | { 58 | return call_user_func_array([$this->uploadedFile, $method], $parameters); 59 | } 60 | 61 | /** 62 | * Method for determining whether the uploaded file is 63 | * an image type. 64 | * 65 | * @return bool 66 | */ 67 | public function isImage() 68 | { 69 | $mime = $this->getMimeType(); 70 | 71 | // The $imageMimes property contains an array of file extensions and 72 | // their associated MIME types. We will loop through them and look for 73 | // the MIME type of the current SymfonyUploadedFile. 74 | foreach ($this->imageMimes as $imageMime) { 75 | if (in_array($mime, (array) $imageMime)) { 76 | return true; 77 | } 78 | } 79 | 80 | return false; 81 | } 82 | 83 | /** 84 | * Return the name of the file. 85 | * 86 | * @return string 87 | */ 88 | public function getFilename() 89 | { 90 | return $this->uploadedFile->getClientOriginalName(); 91 | } 92 | 93 | /** 94 | * Return the size of the file. 95 | * 96 | * @return string 97 | */ 98 | public function getSize() 99 | { 100 | return $this->uploadedFile->getClientSize(); 101 | } 102 | 103 | /** 104 | * Return the mime type of the file. 105 | * 106 | * @return string 107 | */ 108 | public function getMimeType() 109 | { 110 | return $this->uploadedFile->getMimeType(); 111 | } 112 | 113 | /** 114 | * Validate the uploaded file object. 115 | * 116 | * @throws FileException 117 | */ 118 | public function validate() 119 | { 120 | if (!$this->isValid()) { 121 | throw new FileException($this->getErrorMessage()); 122 | } 123 | } 124 | 125 | /** 126 | * Returns an informative upload error message. 127 | * 128 | * @return string 129 | */ 130 | protected function getErrorMessage() 131 | { 132 | $errorCode = $this->getError(); 133 | 134 | static $errors = [ 135 | UPLOAD_ERR_INI_SIZE => 'The file "%s" exceeds your upload_max_filesize ini directive (limit is %d kb).', 136 | UPLOAD_ERR_FORM_SIZE => 'The file "%s" exceeds the upload limit defined in your form.', 137 | UPLOAD_ERR_PARTIAL => 'The file "%s" was only partially uploaded.', 138 | UPLOAD_ERR_NO_FILE => 'No file was uploaded.', 139 | UPLOAD_ERR_CANT_WRITE => 'The file "%s" could not be written on disk.', 140 | UPLOAD_ERR_NO_TMP_DIR => 'File could not be uploaded: missing temporary directory.', 141 | UPLOAD_ERR_EXTENSION => 'File upload was stopped by a php extension.', 142 | ]; 143 | 144 | $maxFilesize = $errorCode === UPLOAD_ERR_INI_SIZE ? self::getMaxFilesize() / 1024 : 0; 145 | $message = isset($errors[$errorCode]) ? $errors[$errorCode] : 'The file "%s" was not uploaded due to an unknown error.'; 146 | 147 | return sprintf($message, $this->getClientOriginalName(), $maxFilesize); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/Interfaces/Attachment.php: -------------------------------------------------------------------------------- 1 | _created_at attribute of the model. 150 | * This attribute may conditionally exist on the model, it is not one of the four required fields. 151 | * 152 | * @return string 153 | */ 154 | public function createdAt(); 155 | 156 | /** 157 | * Returns the last modified time of the file as originally assigned to this attachment's model. 158 | * Lives in the _updated_at attribute of the model. 159 | * 160 | * @return string 161 | */ 162 | public function updatedAt(); 163 | 164 | /** 165 | * Returns the content type of the file as originally assigned to this attachment's model. 166 | * Lives in the _content_type attribute of the model. 167 | * 168 | * @return string 169 | */ 170 | public function contentType(); 171 | 172 | /** 173 | * Returns the size of the file as originally assigned to this attachment's model. 174 | * Lives in the _file_size attribute of the model. 175 | * 176 | * @return int 177 | */ 178 | public function size(); 179 | 180 | /** 181 | * Returns the name of the file as originally assigned to this attachment's model. 182 | * Lives in the _file_name attribute of the model. 183 | * 184 | * @return string 185 | */ 186 | public function originalFilename(); 187 | 188 | /** 189 | * Returns the class type of the attachment's underlying 190 | * model instance. 191 | * 192 | * @return string 193 | */ 194 | public function getInstanceClass(); 195 | 196 | /** 197 | * Rebuilds the images for this attachment. 198 | */ 199 | public function reprocess(); 200 | 201 | /** 202 | * Process the write queue. 203 | * 204 | * @param StaplerableInterface $instance 205 | */ 206 | public function afterSave(StaplerableInterface $instance); 207 | 208 | /** 209 | * Queue up this attachments files for deletion. 210 | * 211 | * @param StaplerableInterface $instance 212 | */ 213 | public function beforeDelete(StaplerableInterface $instance); 214 | 215 | /** 216 | * Process the delete queue. 217 | * 218 | * @param StaplerableInterface $instance 219 | */ 220 | public function afterDelete(StaplerableInterface $instance); 221 | 222 | /** 223 | * Removes all uploaded files (from storage) for this attachment. 224 | * This method does not clear out attachment attributes on the model instance. 225 | * 226 | * @param array $stylesToClear 227 | */ 228 | public function destroy(array $stylesToClear = []); 229 | 230 | /** 231 | * Queues up all or some of this attachments uploaded files/images for deletion. 232 | * 233 | * @param array $stylesToClear 234 | */ 235 | public function clear(array $stylesToClear = []); 236 | 237 | /** 238 | * Flushes the queuedForDeletion and queuedForWrite arrays. 239 | */ 240 | public function save(); 241 | 242 | /** 243 | * Set an attachment attribute on the underlying model instance. 244 | * 245 | * @param string $property 246 | * @param mixed $value 247 | */ 248 | public function instanceWrite($property, $value); 249 | 250 | /** 251 | * Clear (set to null) all attachment related model 252 | * attributes. 253 | */ 254 | public function clearAttributes(); 255 | } -------------------------------------------------------------------------------- /src/Interfaces/Config.php: -------------------------------------------------------------------------------- 1 | interpolations() as $key => $value) { 30 | if (strpos($string, $key) !== false) { 31 | $string = preg_replace("/$key\b/", $this->$value($attachment, $styleName), $string); 32 | } 33 | } 34 | 35 | return $string; 36 | } 37 | 38 | /** 39 | * Returns a sorted list of all interpolations. This list is currently hard coded 40 | * (unlike its paperclip counterpart) but can be changed in the future so that 41 | * all interpolation methods are broken off into their own class and returned automatically. 42 | * 43 | * @return array 44 | */ 45 | protected function interpolations() 46 | { 47 | return [ 48 | ':filename' => 'filename', 49 | ':url' => 'url', 50 | ':app_root' => 'appRoot', 51 | ':class' => 'getClass', 52 | ':class_name' => 'getClassName', 53 | ':namespace' => 'getNamespace', 54 | ':basename' => 'basename', 55 | ':extension' => 'extension', 56 | ':id' => 'id', 57 | ':hash' => 'hash', 58 | ':secure_hash' => 'secureHash', 59 | ':id_partition' => 'idPartition', 60 | ':attachment' => 'attachment', 61 | ':style' => 'style', 62 | ]; 63 | } 64 | 65 | /** 66 | * Returns the file name. 67 | * 68 | * @param AttachmentInterface $attachment 69 | * @param string $styleName 70 | * 71 | * @return string 72 | */ 73 | protected function filename(AttachmentInterface $attachment, $styleName = '') 74 | { 75 | return $attachment->originalFilename(); 76 | } 77 | 78 | /** 79 | * Generates the url to a file upload. 80 | * 81 | * @param AttachmentInterface $attachment 82 | * @param string $styleName 83 | * 84 | * @return string 85 | */ 86 | protected function url(AttachmentInterface $attachment, $styleName = '') 87 | { 88 | return $this->interpolate($attachment->url, $attachment, $styleName); 89 | } 90 | 91 | /** 92 | * Returns the application root of the project. 93 | * 94 | * @param AttachmentInterface $attachment 95 | * @param string $styleName 96 | * 97 | * @return string 98 | */ 99 | protected function appRoot(AttachmentInterface $attachment, $styleName = '') 100 | { 101 | return $attachment->base_path; 102 | } 103 | 104 | /** 105 | * Returns the current class name, taking into account namespaces, e.g 106 | * 'Swingline\Stapler' will become Swingline/Stapler. 107 | * 108 | * @param AttachmentInterface $attachment 109 | * @param string $styleName 110 | * 111 | * @return string 112 | */ 113 | protected function getClass(AttachmentInterface $attachment, $styleName = '') 114 | { 115 | return $this->handleBackslashes($attachment->getInstanceClass()); 116 | } 117 | 118 | /** 119 | * Returns the current class name, not taking into account namespaces, e.g 120 | * 'Swingline\Stapler' will become Stapler. 121 | * 122 | * @param AttachmentInterface $attachment 123 | * @param string $styleName 124 | * 125 | * @return string 126 | */ 127 | protected function getClassName(AttachmentInterface $attachment, $styleName = '') 128 | { 129 | $classComponents = explode('\\', $attachment->getInstanceClass()); 130 | 131 | return end($classComponents); 132 | } 133 | 134 | /** 135 | * Returns the current class name, exclusively taking into account namespaces, e.g 136 | * 'Swingline\Stapler' will become Swingline. 137 | * 138 | * @param AttachmentInterface $attachment 139 | * @param string $styleName 140 | * 141 | * @return string 142 | */ 143 | protected function getNamespace(AttachmentInterface $attachment, $styleName = '') 144 | { 145 | $classComponents = explode('\\', $attachment->getInstanceClass()); 146 | 147 | return implode('/', array_slice($classComponents, 0, count($classComponents) - 1)); 148 | } 149 | 150 | /** 151 | * Returns the basename portion of the attached file, e.g 'file' for file.jpg. 152 | * 153 | * @param AttachmentInterface $attachment 154 | * @param string $styleName 155 | * 156 | * @return string 157 | */ 158 | protected function basename(AttachmentInterface $attachment, $styleName = '') 159 | { 160 | return pathinfo($attachment->originalFilename(), PATHINFO_FILENAME); 161 | } 162 | 163 | /** 164 | * Returns the extension of the attached file, e.g 'jpg' for file.jpg. 165 | * 166 | * @param AttachmentInterface $attachment 167 | * @param string $styleName 168 | * 169 | * @return string 170 | */ 171 | protected function extension(AttachmentInterface $attachment, $styleName = '') 172 | { 173 | return pathinfo($attachment->originalFilename(), PATHINFO_EXTENSION); 174 | } 175 | 176 | /** 177 | * Returns the id of the current object instance. 178 | * 179 | * @param AttachmentInterface $attachment 180 | * @param string $styleName 181 | * 182 | * @return string 183 | */ 184 | protected function id(AttachmentInterface $attachment, $styleName = '') 185 | { 186 | return $this->ensurePrintable($attachment->getInstance()->getKey()); 187 | } 188 | 189 | /** 190 | * Return a secure Bcrypt hash of the attachment's corresponding instance id. 191 | * 192 | * @param AttachmentInterface $attachment 193 | * @param string $styleName 194 | */ 195 | protected function secureHash(AttachmentInterface $attachment, $styleName = '') 196 | { 197 | return hash('sha256', $this->id($attachment, $styleName).$attachment->size().$attachment->originalFilename()); 198 | } 199 | 200 | /** 201 | * Return a Bcrypt hash of the attachment's corresponding instance id. 202 | * 203 | * @param AttachmentInterface $attachment 204 | * @param string $styleName 205 | */ 206 | protected function hash(AttachmentInterface $attachment, $styleName = '') 207 | { 208 | return hash('sha256', $this->id($attachment, $styleName)); 209 | } 210 | 211 | /** 212 | * Generates the id partition of a record, e.g 213 | * return /000/001/234 for an id of 1234. 214 | * 215 | * @param AttachmentInterface $attachment 216 | * @param string $styleName 217 | * 218 | * @return mixed 219 | */ 220 | protected function idPartition(AttachmentInterface $attachment, $styleName = '') 221 | { 222 | $id = $this->ensurePrintable($attachment->getInstance()->getKey()); 223 | 224 | if (is_numeric($id)) { 225 | return implode('/', str_split(sprintf('%09d', $id), 3)); 226 | } elseif (is_string($id)) { 227 | return implode('/', array_slice(str_split($id, 3), 0, 3)); 228 | } else { 229 | return; 230 | } 231 | } 232 | 233 | /** 234 | * Returns the pluralized form of the attachment name. e.g. 235 | * "avatars" for an attachment of :avatar. 236 | * 237 | * @param AttachmentInterface $attachment 238 | * @param string $styleName 239 | * 240 | * @return string 241 | */ 242 | protected function attachment(AttachmentInterface $attachment, $styleName = '') 243 | { 244 | return Inflector::pluralize($attachment->name); 245 | } 246 | 247 | /** 248 | * Returns the style, or the default style if an empty style is supplied. 249 | * 250 | * @param AttachmentInterface $attachment 251 | * @param string $styleName 252 | * 253 | * @return string 254 | */ 255 | protected function style(AttachmentInterface $attachment, $styleName = '') 256 | { 257 | return $styleName ?: $attachment->default_style; 258 | } 259 | 260 | /** 261 | * Utitlity function to turn a backslashed string into a string 262 | * suitable for use in a file path, e.g '\foo\bar' becomes 'foo/bar'. 263 | * 264 | * @param string $string 265 | * 266 | * @return string 267 | */ 268 | protected function handleBackslashes($string) 269 | { 270 | return str_replace('\\', '/', ltrim($string, '\\')); 271 | } 272 | 273 | /** 274 | * Utility method to ensure the input data only contains 275 | * printable characters. This is especially important when 276 | * handling non-printable ID's such as binary UUID's. 277 | * 278 | * @param mixed $input 279 | * 280 | * @return mixed 281 | */ 282 | protected function ensurePrintable($input) 283 | { 284 | if (!is_numeric($input) && !ctype_print($input)) { 285 | // Hash the input data with SHA-256 to represent 286 | // as printable characters, with minimum chances 287 | // of the uniqueness being lost. 288 | return hash('sha256', $input); 289 | } 290 | 291 | return $input; 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /src/ORM/EloquentTrait.php: -------------------------------------------------------------------------------- 1 | attachedFiles; 24 | } 25 | 26 | /** 27 | * Add a new file attachment type to the list of available attachments. 28 | * This function acts as a quasi constructor for this trait. 29 | * 30 | * @param string $name 31 | * @param array $options 32 | */ 33 | public function hasAttachedFile($name, array $options = []) 34 | { 35 | $attachment = AttachmentFactory::create($name, $options); 36 | $attachment->setInstance($this); 37 | $this->attachedFiles[$name] = $attachment; 38 | } 39 | 40 | /** 41 | * The "booting" method of the model. 42 | */ 43 | public static function boot() 44 | { 45 | parent::boot(); 46 | 47 | static::bootStapler(); 48 | } 49 | 50 | /** 51 | * Register eloquent event handlers. 52 | * We'll spin through each of the attached files defined on this class 53 | * and register callbacks for the events we need to observe in order to 54 | * handle file uploads. 55 | */ 56 | public static function bootStapler() 57 | { 58 | static::saved(function ($instance) { 59 | foreach ($instance->attachedFiles as $attachedFile) { 60 | $attachedFile->afterSave($instance); 61 | } 62 | }); 63 | 64 | static::deleting(function ($instance) { 65 | foreach ($instance->attachedFiles as $attachedFile) { 66 | $attachedFile->beforeDelete($instance); 67 | } 68 | }); 69 | 70 | static::deleted(function ($instance) { 71 | foreach ($instance->attachedFiles as $attachedFile) { 72 | $attachedFile->afterDelete($instance); 73 | } 74 | }); 75 | } 76 | 77 | /** 78 | * Handle the dynamic retrieval of attachment objects. 79 | * 80 | * @param string $key 81 | * 82 | * @return mixed 83 | */ 84 | public function getAttribute($key) 85 | { 86 | if (array_key_exists($key, $this->attachedFiles)) { 87 | return $this->attachedFiles[$key]; 88 | } 89 | 90 | return parent::getAttribute($key); 91 | } 92 | 93 | /** 94 | * Handle the dynamic setting of attachment objects. 95 | * 96 | * @param string $key 97 | * @param mixed $value 98 | */ 99 | public function setAttribute($key, $value) 100 | { 101 | if (array_key_exists($key, $this->attachedFiles)) { 102 | if ($value) { 103 | $attachedFile = $this->attachedFiles[$key]; 104 | $attachedFile->setUploadedFile($value); 105 | } 106 | 107 | return; 108 | } 109 | 110 | parent::setAttribute($key, $value); 111 | } 112 | 113 | /** 114 | * Get all of the current attributes and attachment objects on the model. 115 | * 116 | * @return mixed 117 | */ 118 | public function getAttributes() 119 | { 120 | return array_merge($this->attachedFiles, parent::getAttributes()); 121 | } 122 | 123 | /** 124 | * Return the image paths (across all styles) for a given attachment. 125 | * 126 | * @param string $attachmentName 127 | * @return array 128 | */ 129 | public function pathsForAttachment($attachmentName) 130 | { 131 | $paths = []; 132 | 133 | if (isset($this->attachedFiles[$attachmentName])) { 134 | $attachment = $this->attachedFiles[$attachmentName]; 135 | 136 | foreach ($attachment->styles as $style) { 137 | $paths[$style->name] = $attachment->path($style->name); 138 | } 139 | } 140 | 141 | return $paths; 142 | } 143 | 144 | /** 145 | * Return the image urls (across all styles) for a given attachment. 146 | * 147 | * @param string $attachmentName 148 | * @return array 149 | */ 150 | public function urlsForAttachment($attachmentName) 151 | { 152 | $urls = []; 153 | 154 | if (isset($this->attachedFiles[$attachmentName])) { 155 | $attachment = $this->attachedFiles[$attachmentName]; 156 | 157 | foreach ($attachment->styles as $style) { 158 | $urls[$style->name] = $attachment->url($style->name); 159 | } 160 | } 161 | 162 | return $urls; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/ORM/StaplerableInterface.php: -------------------------------------------------------------------------------- 1 | 20 | * 21 | * @link 22 | */ 23 | class Stapler 24 | { 25 | /** 26 | * Holds the hash value for the current STAPLER_NULL constant. 27 | * 28 | * @var string 29 | */ 30 | protected static $staplerNull; 31 | 32 | /** 33 | * An instance of the interpolator class for processing interpolations. 34 | * 35 | * @var \Codesleeve\Stapler\Interfaces\Interpolator 36 | */ 37 | protected static $interpolator; 38 | 39 | /** 40 | * An instance of the validator class for validating attachment configurations. 41 | * 42 | * @var \Codesleeve\Stapler\Interfaces\Validator 43 | */ 44 | protected static $validator; 45 | 46 | /** 47 | * An instance of the resizer class for processing images. 48 | * 49 | * @var \Codesleeve\Stapler\Interfaces\Resizer 50 | */ 51 | protected static $resizer; 52 | 53 | /** 54 | * A configuration object instance. 55 | * 56 | * @var ConfigInterface 57 | */ 58 | protected static $config; 59 | 60 | /** 61 | * An array of image processing libs. 62 | * Each time an new image processing lib (GD, Gmagick, or Imagick) 63 | * is used, we'll cache it here in order to prevent 64 | * memory leaks. 65 | * 66 | * @var array 67 | */ 68 | protected static $imageProcessors = []; 69 | 70 | /** 71 | * A key value store of S3 clients. 72 | * Because S3 clients are model-attachment specific, each 73 | * time we create a new one (for a given model/attachment combo) 74 | * we'll need to cache it here in order to prevent 75 | * memory leaks. 76 | * 77 | * @var array 78 | */ 79 | protected static $s3Clients = []; 80 | 81 | /** 82 | * Boot up stapler. 83 | * Here, we'll register any needed constants and prime up 84 | * the settings required by the package. 85 | */ 86 | public static function boot() 87 | { 88 | static::$staplerNull = sha1(time()); 89 | 90 | if (!defined('STAPLER_NULL')) { 91 | define('STAPLER_NULL', static::$staplerNull); 92 | } 93 | } 94 | 95 | /** 96 | * Return a shared of instance of the Interpolator class. 97 | * If there's currently no instance in memory we'll create one 98 | * and then hang it as a property on this class. 99 | * 100 | * @return \Codesleeve\Stapler\Interfaces\Interpolator 101 | */ 102 | public static function getInterpolatorInstance() 103 | { 104 | if (static::$interpolator === null) { 105 | $className = static::$config->get('bindings.interpolator'); 106 | static::$interpolator = new $className(); 107 | } 108 | 109 | return static::$interpolator; 110 | } 111 | 112 | /** 113 | * Return a shared of instance of the Validator class. 114 | * If there's currently no instance in memory we'll create one 115 | * and then hang it as a property on this class. 116 | * 117 | * @return \Codesleeve\Stapler\Interfaces\Validator 118 | */ 119 | public static function getValidatorInstance() 120 | { 121 | if (static::$validator === null) { 122 | $className = static::$config->get('bindings.validator'); 123 | static::$validator = new $className(); 124 | } 125 | 126 | return static::$validator; 127 | } 128 | 129 | /** 130 | * Return a resizer object instance. 131 | * 132 | * @param string $type 133 | * 134 | * @return \Codesleeve\Stapler\Interfaces\Resizer 135 | */ 136 | public static function getResizerInstance($type) 137 | { 138 | $imagineInstance = static::getImagineInstance($type); 139 | 140 | if (static::$resizer === null) { 141 | $className = static::$config->get('bindings.resizer'); 142 | static::$resizer = new $className($imagineInstance); 143 | } else { 144 | static::$resizer->setImagine($imagineInstance); 145 | } 146 | 147 | return static::$resizer; 148 | } 149 | 150 | /** 151 | * Return an instance of Imagine interface. 152 | * 153 | * @param string $type 154 | * 155 | * @return \Imagine\Image\ImagineInterface 156 | */ 157 | public static function getImagineInstance($type) 158 | { 159 | if (!isset(static::$imageProcessors[$type])) { 160 | static::$imageProcessors[$type] = new $type(); 161 | } 162 | 163 | return static::$imageProcessors[$type]; 164 | } 165 | 166 | /** 167 | * Return an S3Client object for a specific attachment type. 168 | * If no instance has been defined yet we'll buld one and then 169 | * cache it on the s3Clients property (for the current request only). 170 | * 171 | * @param AttachmentInterface $attachedFile 172 | * 173 | * @return S3Client 174 | */ 175 | public static function getS3ClientInstance(AttachmentInterface $attachedFile) 176 | { 177 | $modelName = $attachedFile->getInstanceClass(); 178 | $attachmentName = $attachedFile->getConfig()->name; 179 | $key = "$modelName.$attachmentName"; 180 | 181 | if (array_key_exists($key, static::$s3Clients)) { 182 | return static::$s3Clients[$key]; 183 | } 184 | 185 | static::$s3Clients[$key] = static::buildS3Client($attachedFile); 186 | 187 | return static::$s3Clients[$key]; 188 | } 189 | 190 | /** 191 | * Return a configuration object instance. 192 | * If no instance is currently set, we'll return an instance 193 | * of Codesleeve\Stapler\Config\NativeConfig. 194 | * 195 | * @return ConfigInterface 196 | */ 197 | public static function getConfigInstance() 198 | { 199 | if (!static::$config) { 200 | static::$config = new Config\NativeConfig(); 201 | } 202 | 203 | return static::$config; 204 | } 205 | 206 | /** 207 | * Set the configuration object instance. 208 | * 209 | * @param ConfigInterface $config 210 | */ 211 | public static function setConfigInstance(ConfigInterface $config) 212 | { 213 | static::$config = $config; 214 | } 215 | 216 | /** 217 | * Build an S3Client instance using the information defined in 218 | * this class's attachedFile object. 219 | * 220 | * @param $attachedFile 221 | * 222 | * @return S3Client 223 | */ 224 | protected static function buildS3Client(AttachmentInterface $attachedFile) 225 | { 226 | return S3Client::factory($attachedFile->s3_client_config); 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /src/Storage/Filesystem.php: -------------------------------------------------------------------------------- 1 | attachedFile = $attachedFile; 26 | } 27 | 28 | /** 29 | * Return the url for a file upload. 30 | * 31 | * @param string $styleName 32 | * 33 | * @return string 34 | */ 35 | public function url($styleName) 36 | { 37 | return $this->attachedFile->getInterpolator()->interpolate($this->attachedFile->url, $this->attachedFile, $styleName); 38 | } 39 | 40 | /** 41 | * Return the path (on disk) of a file upload. 42 | * 43 | * @param string $styleName 44 | * 45 | * @return string 46 | */ 47 | public function path($styleName) 48 | { 49 | return $this->attachedFile->getInterpolator()->interpolate($this->attachedFile->path, $this->attachedFile, $styleName); 50 | } 51 | 52 | /** 53 | * Remove an attached file. 54 | * 55 | * @param array $filePaths 56 | */ 57 | public function remove(array $filePaths) 58 | { 59 | foreach ($filePaths as $filePath) { 60 | $directory = dirname($filePath); 61 | $this->emptyDirectory($directory, true); 62 | } 63 | } 64 | 65 | /** 66 | * Move an uploaded file to it's intended destination. 67 | * The file can be an actual uploaded file object or the path to 68 | * a resized image file on disk. 69 | * 70 | * @param string $file 71 | * @param string $filePath 72 | */ 73 | public function move($file, $filePath) 74 | { 75 | $this->buildDirectory($filePath); 76 | $this->moveFile($file, $filePath); 77 | $this->setPermissions($filePath, $this->attachedFile->override_file_permissions); 78 | } 79 | 80 | /** 81 | * Determine if a style directory needs to be built and if so create it. 82 | * 83 | * @param string $filePath 84 | */ 85 | protected function buildDirectory($filePath) 86 | { 87 | $directory = dirname($filePath); 88 | 89 | if (!is_dir($directory)) { 90 | mkdir($directory, 0777, true); 91 | } 92 | } 93 | 94 | /** 95 | * Set the file permissions of a file upload 96 | * Does not ignore umask. 97 | * 98 | * @param string $filePath 99 | * @param int $overrideFilePermissions 100 | */ 101 | protected function setPermissions($filePath, $overrideFilePermissions) 102 | { 103 | if ($overrideFilePermissions) { 104 | chmod($filePath, $overrideFilePermissions & ~umask()); 105 | } elseif (is_null($overrideFilePermissions)) { 106 | chmod($filePath, 0666 & ~umask()); 107 | } 108 | } 109 | 110 | /** 111 | * Attempt to move and uploaded file to it's intended location on disk. 112 | * 113 | * @param string $file 114 | * @param string $filePath 115 | * 116 | * @throws Exceptions\FileException 117 | */ 118 | protected function moveFile($file, $filePath) 119 | { 120 | if (!@rename($file, $filePath)) { 121 | $error = error_get_last(); 122 | throw new Exceptions\FileException(sprintf('Could not move the file "%s" to "%s" (%s)', $file, $filePath, strip_tags($error['message']))); 123 | } 124 | } 125 | 126 | /** 127 | * Recursively delete the files in a directory. 128 | * 129 | * @desc Recursively loops through each file in the directory and deletes it. 130 | * 131 | * @param string $directory 132 | * @param bool $deleteDirectory 133 | */ 134 | protected function emptyDirectory($directory, $deleteDirectory = false) 135 | { 136 | if (!is_dir($directory) || !($directoryHandle = opendir($directory))) { 137 | return; 138 | } 139 | 140 | while (false !== ($object = readdir($directoryHandle))) { 141 | if ($object == '.' || $object == '..') { 142 | continue; 143 | } 144 | 145 | if (!is_dir($directory.'/'.$object)) { 146 | unlink($directory.'/'.$object); 147 | } else { 148 | $this->emptyDirectory($directory.'/'.$object, true); // The object is a folder, recurse through it. 149 | } 150 | } 151 | 152 | if ($deleteDirectory) { 153 | closedir($directoryHandle); 154 | rmdir($directory); 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/Storage/S3.php: -------------------------------------------------------------------------------- 1 | attachedFile = $attachedFile; 41 | $this->s3Client = $s3Client; 42 | } 43 | 44 | /** 45 | * Return the url for a file upload. 46 | * 47 | * @param string $styleName 48 | * 49 | * @return string 50 | */ 51 | public function url($styleName) 52 | { 53 | return $this->s3Client->getObjectUrl($this->attachedFile->s3_object_config['Bucket'], $this->path($styleName), null, ['PathStyle' => true]); 54 | } 55 | 56 | /** 57 | * Return the key the uploaded file object is stored under within a bucket. 58 | * 59 | * @param string $styleName 60 | * 61 | * @return string 62 | */ 63 | public function path($styleName) 64 | { 65 | return $this->attachedFile->getInterpolator()->interpolate($this->attachedFile->path, $this->attachedFile, $styleName); 66 | } 67 | 68 | /** 69 | * Remove an attached file. 70 | * 71 | * @param array $filePaths 72 | */ 73 | public function remove(array $filePaths) 74 | { 75 | if ($filePaths) { 76 | $this->s3Client->deleteObjects(['Bucket' => $this->attachedFile->s3_object_config['Bucket'], 'Objects' => $this->getKeys($filePaths)]); 77 | } 78 | } 79 | 80 | /** 81 | * Move an uploaded file to it's intended destination. 82 | * 83 | * @param string $file 84 | * @param string $filePath 85 | */ 86 | public function move($file, $filePath) 87 | { 88 | $objectConfig = $this->attachedFile->s3_object_config; 89 | $fileSpecificConfig = ['Key' => $filePath, 'SourceFile' => $file, 'ContentType' => $this->attachedFile->contentType()]; 90 | $mergedConfig = array_merge($objectConfig, $fileSpecificConfig); 91 | 92 | $this->ensureBucketExists($mergedConfig['Bucket']); 93 | $this->s3Client->putObject($mergedConfig); 94 | 95 | @unlink($file); 96 | } 97 | 98 | /** 99 | * Return an array of paths (bucket keys) for an attachment. 100 | * There will be one path for each of the attachmetn's styles. 101 | * 102 | * @param $filePaths 103 | * 104 | * @return array 105 | */ 106 | protected function getKeys($filePaths) 107 | { 108 | $keys = []; 109 | 110 | foreach ($filePaths as $filePath) { 111 | $keys[] = ['Key' => $filePath]; 112 | } 113 | 114 | return $keys; 115 | } 116 | 117 | /** 118 | * Ensure that a given S3 bucket exists. 119 | * 120 | * @param string $bucketName 121 | */ 122 | protected function ensureBucketExists($bucketName) 123 | { 124 | if (!$this->bucketExists) { 125 | $this->buildBucket($bucketName); 126 | } 127 | } 128 | 129 | /** 130 | * Attempt to build a bucket (if it doesn't already exist). 131 | * 132 | * @param string $bucketName 133 | */ 134 | protected function buildBucket($bucketName) 135 | { 136 | if (!$this->s3Client->doesBucketExist($bucketName, true)) { 137 | $this->s3Client->createBucket(['ACL' => $this->attachedFile->ACL, 'Bucket' => $bucketName, 'LocationConstraint' => $this->attachedFile->region]); 138 | } 139 | 140 | $this->bucketExists = true; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/Style.php: -------------------------------------------------------------------------------- 1 | name = $name; 51 | 52 | if (is_array($value)) { 53 | if (!array_key_exists('dimensions', $value)) { 54 | throw new Exceptions\InvalidStyleConfigurationException('Error Processing Request', 1); 55 | } 56 | 57 | $this->dimensions = $value['dimensions']; 58 | 59 | if (array_key_exists('auto_orient', $value)) { 60 | $this->autoOrient = $value['auto_orient']; 61 | } 62 | 63 | if (array_key_exists('convert_options', $value)) { 64 | $this->convertOptions = $value['convert_options']; 65 | } 66 | 67 | return; 68 | } 69 | 70 | $this->dimensions = $value; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Validator.php: -------------------------------------------------------------------------------- 1 | validateFilesystemOptions($options) : $this->validateS3Options($options); 18 | } 19 | 20 | /** 21 | * Validate the attachment options for an attachment type when the storage 22 | * driver is set to 'filesystem'. 23 | * 24 | * @throws Exceptions\InvalidUrlOptionException 25 | * 26 | * @param array $options 27 | */ 28 | protected function validateFilesystemOptions(array $options) 29 | { 30 | if (preg_match("/:id\b/", $options['url']) !== 1 && preg_match("/:id_partition\b/", $options['url']) !== 1 && preg_match("/:(secure_)?hash\b/", $options['url']) !== 1) { 31 | throw new Exceptions\InvalidUrlOptionException('Invalid Url: an id, id_partition, hash, or secure_hash interpolation is required.', 1); 32 | } 33 | } 34 | 35 | /** 36 | * Validate the attachment options for an attachment type when the storage 37 | * driver is set to 's3'. 38 | * 39 | * @throws Exceptions\InvalidUrlOptionException 40 | * 41 | * @param array $options 42 | */ 43 | protected function validateS3Options(array $options) 44 | { 45 | if (!$options['s3_object_config']['Bucket']) { 46 | throw new Exceptions\InvalidUrlOptionException('Invalid Path: a bucket is required for s3 storage.', 1); 47 | } 48 | 49 | if (!$options['s3_client_config']['secret']) { 50 | throw new Exceptions\InvalidUrlOptionException('Invalid Path: a secret is required for s3 storage.', 1); 51 | } 52 | 53 | if (!$options['s3_client_config']['key']) { 54 | throw new Exceptions\InvalidUrlOptionException('Invalid Path: a key is required for s3 storage.', 1); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeSleeve/stapler/ba7e2b5f2d7ab7545fbfe7537c1e3bd9f32d36f8/tests/.gitkeep -------------------------------------------------------------------------------- /tests/Codesleeve/Stapler/AttachmentConfigTest.php: -------------------------------------------------------------------------------- 1 | []]); 35 | 36 | $this->assertEquals('mockAttachment', $attachmentConfig->name); 37 | } 38 | 39 | /** 40 | * Test that the AttachmentConfig class can dynamically store attachment 41 | * config options. 42 | * 43 | * @test 44 | */ 45 | public function it_should_be_able_to_dynamically_retrieve_config_values() 46 | { 47 | $attachmentConfig = new AttachmentConfig('mockAttachment', ['styles' => [], 'foo' => 'bar']); 48 | 49 | $this->assertEquals('bar', $attachmentConfig->foo); 50 | $this->assertNull($attachmentConfig->baz); 51 | } 52 | 53 | /** 54 | * Test that the AttachmentConfig class can dynamically set new config options. 55 | * 56 | * @test 57 | */ 58 | public function it_should_be_able_to_dynamically_set_config_values() 59 | { 60 | $attachmentConfig = new AttachmentConfig('mockAttachment', ['styles' => []]); 61 | 62 | $attachmentConfig->foo = 'bar'; 63 | 64 | $this->assertEquals('bar', $attachmentConfig->foo); 65 | } 66 | 67 | /** 68 | * Test that the AttachmentConfig class can build an 69 | * array of style objects if style options are passed in. 70 | * 71 | * @test 72 | * 73 | * @param string $value [description] 74 | */ 75 | public function it_should_be_able_build_an_array_of_style_objects() 76 | { 77 | $attachmentConfig = new AttachmentConfig('mockAttachment', ['styles' => ['baz' => '']]); 78 | 79 | $this->assertTrue(is_array($attachmentConfig->styles)); 80 | $this->assertInstanceOf('Codesleeve\Stapler\Interfaces\Style', $attachmentConfig->styles[0]); 81 | } 82 | 83 | /** 84 | * Test that the AttachmentConfig class will throw an exception 85 | * if no styles key is present in the options array. 86 | * 87 | * @test 88 | * @expectedException \Codesleeve\Stapler\Exceptions\InvalidAttachmentConfigurationException 89 | */ 90 | public function it_should_throw_an_exception_if_no_styles_key_is_present() 91 | { 92 | $attachmentConfig = new AttachmentConfig('mockAttachment', []); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /tests/Codesleeve/Stapler/AttachmentTest.php: -------------------------------------------------------------------------------- 1 | build_attachment(); 39 | 40 | $staplerUploadedFile = $attachment->setUploadedFile(STAPLER_NULL); 41 | 42 | $this->assertNull($staplerUploadedFile); 43 | } 44 | 45 | /** 46 | * Calling the url method with a style parameter should 47 | * return the url for that style. 48 | * 49 | * @test 50 | */ 51 | public function it_should_be_able_to_return_an_attachment_url_for_a_style() 52 | { 53 | $attachment = $this->build_attachment(); 54 | $symfonyUploadedFile = new SymfonyUploadedFile(__DIR__.'/Fixtures/empty.gif', 'empty.gif', null, null, null, true); 55 | $staplerUploadedFile = $attachment->setUploadedFile($symfonyUploadedFile); 56 | 57 | $url = $attachment->url('thumbnail'); 58 | 59 | $this->assertEquals('/system/photos/000/000/001/thumbnail/empty.gif', $url); 60 | } 61 | 62 | /** 63 | * Calling the url method without a style parameter should 64 | * return the url for the default style. 65 | * 66 | * @test 67 | */ 68 | public function it_should_be_able_to_return_the_default_url_for_an_attachment_if_no_style_is_given() 69 | { 70 | $attachment = $this->build_attachment(); 71 | $symfonyUploadedFile = new SymfonyUploadedFile(__DIR__.'/Fixtures/empty.gif', 'empty.gif', null, null, null, true); 72 | $staplerUploadedFile = $attachment->setUploadedFile($symfonyUploadedFile); 73 | 74 | $url = $attachment->url(); 75 | 76 | $this->assertEquals('/system/photos/000/000/001/original/empty.gif', $url); 77 | } 78 | 79 | /** 80 | * Calling the path method with a style parameter should 81 | * return the path for that style. 82 | * 83 | * @test 84 | */ 85 | public function it_should_be_able_to_return_an_attachment_path_for_a_style() 86 | { 87 | $attachment = $this->build_attachment(); 88 | $symfonyUploadedFile = new SymfonyUploadedFile(__DIR__.'/Fixtures/empty.gif', 'empty.gif', null, null, null, true); 89 | $staplerUploadedFile = $attachment->setUploadedFile($symfonyUploadedFile); 90 | 91 | $path = $attachment->path('thumbnail'); 92 | 93 | $this->assertEquals('/public/system/photos/000/000/001/thumbnail/empty.gif', $path); 94 | } 95 | 96 | /** 97 | * Calling the path method without a style parameter should 98 | * return the path for the default style. 99 | * 100 | * @test 101 | */ 102 | public function it_should_be_able_to_return_the_default_path_for_an_attachment_if_no_style_is_given($value = '') 103 | { 104 | $attachment = $this->build_attachment(); 105 | $symfonyUploadedFile = new SymfonyUploadedFile(__DIR__.'/Fixtures/empty.gif', 'empty.gif', null, null, null, true); 106 | $staplerUploadedFile = $attachment->setUploadedFile($symfonyUploadedFile); 107 | 108 | $path = $attachment->path(); 109 | 110 | $this->assertEquals('/public/system/photos/000/000/001/original/empty.gif', $path); 111 | } 112 | 113 | /** 114 | * Calling the contentType method should return the 115 | * content type of the original uploaded file. 116 | * 117 | * @test 118 | */ 119 | public function it_should_be_able_to_return_the_content_type() 120 | { 121 | $attachment = $this->build_attachment(); 122 | $symfonyUploadedFile = new SymfonyUploadedFile(__DIR__.'/Fixtures/empty.gif', 'empty.gif', null, null, null, true); 123 | $staplerUploadedFile = $attachment->setUploadedFile($symfonyUploadedFile); 124 | 125 | $contentType = $attachment->contentType(); 126 | 127 | $this->assertEquals('image/gif', $contentType); 128 | } 129 | 130 | /** 131 | * Calling the size method should return the size of the 132 | * original uploaded file. 133 | * 134 | * @test 135 | */ 136 | public function it_should_be_able_to_return_the_originaL_file_size() 137 | { 138 | $attachment = $this->build_attachment(); 139 | $symfonyUploadedFile = new SymfonyUploadedFile(__DIR__.'/Fixtures/empty.gif', 'empty.gif', null, null, null, true); 140 | $staplerUploadedFile = $attachment->setUploadedFile($symfonyUploadedFile); 141 | 142 | $size = $attachment->size(); 143 | 144 | $this->assertEquals(0, $size); 145 | } 146 | 147 | /** 148 | * Calling the originalFilename method should return the name 149 | * of the original uploaded file. 150 | * 151 | * @test 152 | */ 153 | public function it_should_be_able_to_return_the_original_file_name() 154 | { 155 | $attachment = $this->build_attachment(); 156 | $symfonyUploadedFile = new SymfonyUploadedFile(__DIR__.'/Fixtures/empty.gif', 'empty.gif', null, null, null, true); 157 | $staplerUploadedFile = $attachment->setUploadedFile($symfonyUploadedFile); 158 | 159 | $filename = $attachment->originalFilename(); 160 | 161 | $this->assertEquals('empty.gif', $filename); 162 | } 163 | 164 | /** 165 | * Build an attachment object. 166 | * 167 | * @param \Codesleeve\Stapler\Interpolator 168 | * 169 | * @return \Codesleeve\Stapler\Attachment 170 | */ 171 | protected function build_attachment() 172 | { 173 | $instance = $this->build_mock_instance(); 174 | $interpolator = new Interpolator(); 175 | $attachmentConfig = new \Codesleeve\Stapler\AttachmentConfig('photo', [ 176 | 'styles' => [], 177 | 'default_style' => 'original', 178 | 'url' => '/system/:attachment/:id_partition/:style/:filename', 179 | 'path' => ':app_root/public:url', 180 | ]); 181 | 182 | $imagine = m::mock('Imagine\Image\ImagineInterface'); 183 | $resizer = new \Codesleeve\Stapler\File\Image\Resizer($imagine); 184 | 185 | $attachment = new \Codesleeve\Stapler\Attachment($attachmentConfig, $interpolator, $resizer); 186 | $attachment->setInstance($instance); 187 | 188 | $storageDriver = new \Codesleeve\Stapler\Storage\Filesystem($attachment); 189 | $attachment->setStorageDriver($storageDriver); 190 | 191 | return $attachment; 192 | } 193 | 194 | /** 195 | * Build a mock model instance. 196 | * 197 | * @return mixed 198 | */ 199 | protected function build_mock_instance() 200 | { 201 | $instance = m::mock('Codesleeve\Stapler\ORM\StaplerableInterface'); 202 | $instance->shouldReceive('getKey')->andReturn(1); 203 | $instance->shouldReceive('getAttribute')->with('photo_file_name')->andReturn('empty.gif'); 204 | $instance->shouldReceive('getAttribute')->with('photo_file_size')->andReturn(0); 205 | $instance->shouldReceive('getAttribute')->with('photo_content_type')->andReturn('image/gif'); 206 | $instance->shouldReceive('setAttribute'); 207 | 208 | return $instance; 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /tests/Codesleeve/Stapler/Config/NativeConfigTest.php: -------------------------------------------------------------------------------- 1 | ['item1' => 'foo']]; 34 | $config = new NativeConfig($data); 35 | 36 | $item = $config->get('group1.item1'); 37 | 38 | $this->assertEquals('foo', $item); 39 | } 40 | 41 | /** 42 | * Test that a NativeConfig object can get a group of 43 | * config item. 44 | * 45 | * @test 46 | */ 47 | public function it_should_be_able_to_get_an_item_group() 48 | { 49 | $data = ['group1' => ['item1' => 'foo']]; 50 | $config = new NativeConfig($data); 51 | 52 | $item = $config->get('group1'); 53 | 54 | $this->assertEquals(['item1' => 'foo'], $item); 55 | } 56 | 57 | /** 58 | * Test that a NativeConfig object can get a single 59 | * config item. 60 | * 61 | * @test 62 | */ 63 | public function it_should_be_able_to_set_a_single_item() 64 | { 65 | $config = new NativeConfig([]); 66 | 67 | $config->set('group1.item1', 'bar'); 68 | 69 | $this->assertEquals('bar', $config->get('group1.item1')); 70 | } 71 | 72 | /** 73 | * Test that a NativeConfig object can set a group of 74 | * config item. 75 | * 76 | * @test 77 | */ 78 | public function it_should_be_able_to_set_an_item_group() 79 | { 80 | $config = new NativeConfig([]); 81 | 82 | $item = $config->set('group1', ['item1' => 'foo']); 83 | 84 | $this->assertEquals(['item1' => 'foo'], $config->get('group1')); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /tests/Codesleeve/Stapler/Factories/AttachmentTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf('Codesleeve\Stapler\Interfaces\Attachment', $attachment); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/Codesleeve/Stapler/Factories/FileTest.php: -------------------------------------------------------------------------------- 1 | buildSymfonyUploadedFile(true); 33 | 34 | $uploadedFile = File::create($symfonyUploadedFile); 35 | 36 | $this->assertInstanceOf('Codesleeve\Stapler\Interfaces\File', $uploadedFile); 37 | } 38 | 39 | /** 40 | * Test that the file factory can create a Codesleeve\Stapler\UploadedFile 41 | * object from an array. 42 | * 43 | * @test 44 | */ 45 | public function it_should_be_able_to_build_a_stapler_uploaded_file_object_from_an_array() 46 | { 47 | $fileData = [ 48 | 'tmp_name' => __DIR__.'/../Fixtures/empty.gif', 49 | 'name' => 'empty.gif', 50 | 'type' => null, 51 | 'size' => null, 52 | 'error' => null, 53 | ]; 54 | 55 | $uploadedFile = File::create($fileData, true); 56 | 57 | $this->assertInstanceOf('Codesleeve\Stapler\Interfaces\File', $uploadedFile); 58 | } 59 | 60 | /** 61 | * Test that the file factory can create a Codesleeve\Stapler\UploadedFile 62 | * object from a url. 63 | * 64 | * @test 65 | */ 66 | public function it_should_be_able_to_build_a_stapler_uploaded_file_object_from_a_url() 67 | { 68 | $uploadedFile = File::create('https://www.google.com/images/srpr/logo11w.png'); 69 | 70 | $this->assertInstanceOf('Codesleeve\Stapler\Interfaces\File', $uploadedFile); 71 | } 72 | 73 | /** 74 | * Test that the file factory can create a Codesleeve\Stapler\UploadedFile 75 | * object from a redirect url. 76 | * 77 | * @test 78 | */ 79 | public function it_should_be_able_to_build_a_stapler_uploaded_file_object_from_a_redirect_url() 80 | { 81 | $uploadedFile = File::create('https://graph.facebook.com/zuck/picture?type=large'); 82 | 83 | $this->assertInstanceOf('Codesleeve\Stapler\Interfaces\File', $uploadedFile); 84 | } 85 | 86 | /** 87 | * Test that file created by file factory is not containing unnecessary quer string. 88 | * 89 | * @test 90 | */ 91 | public function it_should_be_able_to_build_a_stapler_uploaded_file_object_without_following_querystring_in_basename() 92 | { 93 | $url = 'https://graph.facebook.com/zuck/picture?type=large'; 94 | $uploadedFile = File::create($url); 95 | 96 | $ch = curl_init($url); 97 | curl_setopt($ch, CURLOPT_URL, $url); 98 | curl_setopt($ch, CURLOPT_HEADER, true); 99 | curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); 100 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 101 | curl_exec($ch); 102 | $info = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL); 103 | curl_close($ch); 104 | 105 | // To make sure that the exact image URL has query string 106 | $this->assertGreaterThanOrEqual(0, strpos($info, '?')); 107 | $this->assertFalse(strpos($uploadedFile->getFileName(), '?')); 108 | } 109 | 110 | /** 111 | * Test that the file factory can create a Codesleeve\Stapler\UploadedFile 112 | * object from a string filepath. 113 | * 114 | * @test 115 | */ 116 | public function it_should_be_able_to_build_a_stapler_uploaded_file_object_from_a_string_file_path() 117 | { 118 | $uploadedFile = File::create(__DIR__.'/../Fixtures/empty.gif'); 119 | 120 | $this->assertInstanceOf('Codesleeve\Stapler\Interfaces\File', $uploadedFile); 121 | } 122 | 123 | /** 124 | * Helper method to build a mock Symfony UploadedFile object. 125 | * 126 | * @param bool $testing 127 | * 128 | * @return UploadedFile 129 | */ 130 | protected function buildSymfonyUploadedFile($testing = true) 131 | { 132 | $path = __DIR__.'/../Fixtures/empty.gif'; 133 | $originalName = 'empty.gif'; 134 | 135 | return new SymfonyUploadedFile($path, $originalName, null, null, null, $testing); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /tests/Codesleeve/Stapler/Factories/StorageTest.php: -------------------------------------------------------------------------------- 1 | buildMockAttachment('filesystem'); 34 | 35 | $storage = Storage::create($attachment); 36 | 37 | $this->assertInstanceOf('Codesleeve\Stapler\Storage\Filesystem', $storage); 38 | } 39 | 40 | /** 41 | * Test that the Storage factory can create an instance of the s3 42 | * storage driver. 43 | * 44 | * @test 45 | */ 46 | public function it_should_be_able_to_create_an_s3_storeage_instance() 47 | { 48 | $attachment = $this->buildMockAttachment('s3'); 49 | 50 | $storage = Storage::create($attachment); 51 | 52 | $this->assertInstanceOf('Codesleeve\Stapler\Storage\S3', $storage); 53 | } 54 | 55 | /** 56 | * Test that the Storage factory should create an instance of the filesystem 57 | * storage driver by default. 58 | * 59 | * @test 60 | */ 61 | public function it_should_be_able_to_create_a_filesystem_storeage_instance_by_default() 62 | { 63 | $attachment = $this->buildMockAttachment(); 64 | 65 | $storage = Storage::create($attachment); 66 | 67 | $this->assertInstanceOf('Codesleeve\Stapler\Storage\Filesystem', $storage); 68 | } 69 | 70 | /** 71 | * Build a mock attachment object. 72 | * 73 | * @param string $type 74 | * 75 | * @return \Codesleeve\Stapler\Attachment 76 | */ 77 | protected function buildMockAttachment($type = null) 78 | { 79 | $attachment = m::mock('Codesleeve\Stapler\Attachment')->makePartial(); 80 | $attachmentConfig = new \Codesleeve\Stapler\AttachmentConfig('testAttachmentConfig', ['styles' => []]); 81 | $attachment->setConfig($attachmentConfig); 82 | $attachment->storage = $type; 83 | 84 | return $attachment; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /tests/Codesleeve/Stapler/File/Image/ResizerTest.php: -------------------------------------------------------------------------------- 1 | buildUploadedFile(); 38 | $originalSize = new Box(600, 400); 39 | $expectedResize = new Box(768, 512); 40 | $expectedCropPoint = new Point(128, 0); 41 | $expectedCropBox = new Box(512, 512); 42 | 43 | $image = $this->buildMockImage($originalSize, $expectedResize, $expectedCropPoint, $expectedCropBox); 44 | $imageProcessor = $this->buildMockImageProcessor($image); 45 | $resizer = new Resizer($imageProcessor); 46 | 47 | $style = $this->buildMockStyleObject('thumbnail', '512x512#'); 48 | $file = $resizer->resize($uploadedFile, $style); 49 | } 50 | 51 | /** 52 | * Test resize cropping edge case. 53 | * 54 | * @test 55 | */ 56 | public function it_should_be_able_to_resize_and_crop_an_edge_case() 57 | { 58 | $uploadedFile = $this->buildUploadedFile(); 59 | $originalSize = new Box(1000, 653); 60 | $expectedResize = new Box(440, 287.32); 61 | $expectedCropPoint = new Point(0, 21.66); 62 | $expectedCropBox = new Box(440, 244); 63 | 64 | $image = $this->buildMockImage($originalSize, $expectedResize, $expectedCropPoint, $expectedCropBox); 65 | $imageProcessor = $this->buildMockImageProcessor($image); 66 | $resizer = new Resizer($imageProcessor); 67 | 68 | $style = $this->buildMockStyleObject('thumbnail', '440x244#'); 69 | $file = $resizer->resize($uploadedFile, $style); 70 | } 71 | 72 | /** 73 | * Helper method to build a mock Stapler UploadedFile object. 74 | * 75 | * @return UploadedFile 76 | */ 77 | protected function buildUploadedFile() 78 | { 79 | $path = realpath(__DIR__.'/../../Fixtures/empty.gif'); 80 | $originalName = 'Test.gif'; 81 | $symfonyUploadedFile = new SymfonyUploadedFile($path, $originalName, null, null, null, true); 82 | 83 | return new UploadedFile($symfonyUploadedFile); 84 | } 85 | 86 | /** 87 | * Helper method to build a mock Image object. 88 | * 89 | * @param int $originalSize 90 | * @param int $expectedResize 91 | * @param int $expectedCropPoint 92 | * @param int $expectedCropBox 93 | * 94 | * @return Image 95 | */ 96 | protected function buildMockImage($originalSize, $expectedResize, $expectedCropPoint = null, $expectedCropBox = null) 97 | { 98 | $image = $this->getMock('Imagine\Image\ImageInterface'); 99 | $image->expects($this->once())->method('getSize')->will($this->returnValue($originalSize)); 100 | $image->expects($this->once())->method('resize')->with($expectedResize)->will($this->returnValue($image)); 101 | $image->expects($this->once())->method('crop')->with($expectedCropPoint, $expectedCropBox)->will($this->returnValue($image)); 102 | $image->expects($this->once())->method('save'); 103 | 104 | return $image; 105 | } 106 | 107 | /** 108 | * Helper method to build a mock Imagine instance. 109 | * 110 | * @param Image $image 111 | * 112 | * @return Imagine 113 | */ 114 | protected function buildMockImageProcessor($image) 115 | { 116 | $imageProcessor = m::mock('Imagine\Image\ImagineInterface'); 117 | $imageProcessor->shouldReceive('open')->once()->andReturn($image); 118 | 119 | return $imageProcessor; 120 | } 121 | 122 | /** 123 | * Helper method to build a mock style object. 124 | * 125 | * @param string $name 126 | * @param string $value 127 | * @param array $convertOptions 128 | * 129 | * @return Object 130 | */ 131 | protected function buildMockStyleObject($name, $value, $convertOptions = []) 132 | { 133 | return new Style($name, $value, $convertOptions); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /tests/Codesleeve/Stapler/File/UploadedFileTest.php: -------------------------------------------------------------------------------- 1 | buildInvalidStaplerUploadedFile(); 37 | 38 | $staplerUploadedFile->validate(); 39 | } 40 | 41 | /** 42 | * An uploaded file shoudl be able to detect if the 43 | * file type that has been uploaded is an image. 44 | * 45 | * @test 46 | */ 47 | public function it_should_be_able_to_detect_the_if_the_file_is_an_image() 48 | { 49 | $staplerUploadedFile = $this->buildValidStaplerUploadedFile(); 50 | 51 | $isImage = $staplerUploadedFile->isImage(); 52 | 53 | $this->assertEquals(true, $isImage); 54 | } 55 | 56 | /** 57 | * An uploaded file object should be able to return the 58 | * name of the underlying uploaded file. 59 | * 60 | * @test 61 | */ 62 | public function it_should_be_able_to_get_the_name_of_the_uploaded_file() 63 | { 64 | $staplerUploadedFile = $this->buildValidStaplerUploadedFile(); 65 | 66 | $filename = $staplerUploadedFile->getFilename(); 67 | 68 | $this->assertEquals('empty.gif', $filename); 69 | } 70 | 71 | /** 72 | * An uploaded file object should be able to return the size of the 73 | * underlying uploaded file. 74 | * 75 | * @test 76 | */ 77 | public function it_should_be_able_to_get_the_size_of_the_uploaded_file() 78 | { 79 | $staplerUploadedFile = $this->buildValidStaplerUploadedFile(); 80 | 81 | $size = $staplerUploadedFile->getSize(); 82 | 83 | $this->assertEquals(null, $size); 84 | } 85 | 86 | /** 87 | * An uploaded file object should be able to return the mime type 88 | * of the underlyjng uploaded file. 89 | * 90 | * @test 91 | */ 92 | public function it_should_be_able_to_get_the_mime_type_of_the_uploaded_file() 93 | { 94 | $staplerUploadedFile = $this->buildValidStaplerUploadedFile(); 95 | 96 | $mime = $staplerUploadedFile->getMimeType(); 97 | 98 | $this->assertEquals('image/gif', $mime); 99 | } 100 | 101 | /** 102 | * Helper method to build an valid Codesleeve\Stapler\File\UploadedFile object. 103 | * 104 | * @return UploadedFile 105 | */ 106 | protected function buildValidStaplerUploadedFile() 107 | { 108 | $symfonyUploadedFile = $this->buildSymfonyUploadedFile(); 109 | 110 | return new UploadedFile($symfonyUploadedFile); 111 | } 112 | 113 | /** 114 | * Helper method to build an invalid Codesleeve\Stapler\File\UploadedFile object. 115 | * 116 | * @return UploadedFile 117 | */ 118 | protected function buildInvalidStaplerUploadedFile() 119 | { 120 | $symfonyUploadedFile = $this->buildSymfonyUploadedFile(false); 121 | 122 | return new UploadedFile($symfonyUploadedFile); 123 | } 124 | 125 | /** 126 | * Helper method to build a mock Symfony UploadedFile object. 127 | * 128 | * @param bool $testing 129 | * 130 | * @return UploadedFile 131 | */ 132 | protected function buildSymfonyUploadedFile($testing = true) 133 | { 134 | $path = __DIR__.'/../Fixtures/empty.gif'; 135 | $originalName = 'empty.gif'; 136 | 137 | return new SymfonyUploadedFile($path, $originalName, null, null, null, $testing); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /tests/Codesleeve/Stapler/Fixtures/Models/Photo.php: -------------------------------------------------------------------------------- 1 | 1]) 21 | { 22 | $this->hasAttachedFile('photo', [ 23 | 'styles' => [ 24 | 'thumbnail' => '100x100', 25 | ], 26 | 'url' => '/system/:attachment/:id_partition/:style/:filename', 27 | 'default_url' => '/defaults/:style/missing.png', 28 | 'convert_options' => [ 29 | 'thumbnail' => ['quality' => 100, 'auto-orient' => true], 30 | ], 31 | ]); 32 | 33 | parent::__construct($attributes); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/Codesleeve/Stapler/Fixtures/empty.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeSleeve/stapler/ba7e2b5f2d7ab7545fbfe7537c1e3bd9f32d36f8/tests/Codesleeve/Stapler/Fixtures/empty.gif -------------------------------------------------------------------------------- /tests/Codesleeve/Stapler/InterpolatorTest.php: -------------------------------------------------------------------------------- 1 | interpolator = $this->interpolator ?: new Interpolator(); 30 | $this->attachment = $this->attachment ?: $this->build_mock_attachment($this->interpolator); 31 | } 32 | 33 | /** 34 | * Teardown method. 35 | */ 36 | public function tearDown() 37 | { 38 | m::close(); 39 | } 40 | 41 | /** 42 | * Test that when no style is passed in, the interpolator 43 | * will correctly interpolate a string using the default style. 44 | * 45 | * @test 46 | */ 47 | public function it_should_be_able_to_interpolate_a_string_using_the_default_style() 48 | { 49 | $input = '/system/:class/:attachment/:id/:style/:filename'; 50 | 51 | $interpolatedString = $this->interpolator->interpolate($input, $this->attachment); 52 | 53 | $this->assertEquals('/system/TestModel/photos/1/original/test.jpg', $interpolatedString); 54 | } 55 | 56 | /** 57 | * Test the interpolator will correctly interpolate a string when 58 | * using an injected style. 59 | * 60 | * @test 61 | */ 62 | public function it_should_be_able_to_interpolate_a_string_using_an_injected_style() 63 | { 64 | $input = '/system/:class/:attachment/:id/:style/:filename'; 65 | 66 | $interpolatedString = $this->interpolator->interpolate($input, $this->attachment, 'thumbnail'); 67 | 68 | $this->assertEquals('/system/TestModel/photos/1/thumbnail/test.jpg', $interpolatedString); 69 | } 70 | 71 | /** 72 | * Test the interpolator will correctly interpolate a string when 73 | * using an id partition. 74 | * 75 | * @test 76 | */ 77 | public function it_should_be_able_to_interpolate_a_string_using_an_id_partition() 78 | { 79 | $input = '/system/:class/:attachment/:id_partition/:style/:filename'; 80 | 81 | $interpolatedString = $this->interpolator->interpolate($input, $this->attachment, 'thumbnail'); 82 | 83 | $this->assertEquals('/system/TestModel/photos/000/000/001/thumbnail/test.jpg', $interpolatedString); 84 | } 85 | 86 | /** 87 | * Test the interpolator will correctly interpolate a string when 88 | * using a class name. 89 | * 90 | * @test 91 | */ 92 | public function it_should_be_able_to_interpolate_a_string_using_a_class_name() 93 | { 94 | $attachment = $this->build_mock_attachment($this->interpolator, 'Foo\\Faz\\Baz\\TestModel'); 95 | $input = '/system/:class_name/:attachment/:id_partition/:style/:filename'; 96 | $interpolatedString = $this->interpolator->interpolate($input, $attachment, 'thumbnail'); 97 | 98 | $this->assertEquals('/system/TestModel/photos/000/000/001/thumbnail/test.jpg', $interpolatedString); 99 | } 100 | 101 | /** 102 | * Test the interpolator will correctly interpolate a string when 103 | * using a namespace. 104 | * 105 | * @test 106 | */ 107 | public function it_should_be_able_to_interpolate_a_string_using_a_namespace() 108 | { 109 | $attachment = $this->build_mock_attachment($this->interpolator, 'Foo\\Faz\\Baz\\TestModel'); 110 | $input = '/system/:namespace/:attachment/:id_partition/:style/:filename'; 111 | $interpolatedString = $this->interpolator->interpolate($input, $attachment, 'thumbnail'); 112 | 113 | $this->assertEquals('/system/Foo/Faz/Baz/photos/000/000/001/thumbnail/test.jpg', $interpolatedString); 114 | } 115 | 116 | /** 117 | * Test the interpolator will correctly interpolate a string when 118 | * using a namespace and class name. 119 | * 120 | * @test 121 | */ 122 | public function it_should_be_able_to_interpolate_a_string_using_a_namespace_and_class_name() 123 | { 124 | $attachment = $this->build_mock_attachment($this->interpolator, 'Foo\\Faz\\Baz\\TestModel'); 125 | $input = '/system/:namespace/:class_name/:attachment/:id_partition/:style/:filename'; 126 | $interpolatedString = $this->interpolator->interpolate($input, $attachment, 'thumbnail'); 127 | 128 | $this->assertEquals('/system/Foo/Faz/Baz/TestModel/photos/000/000/001/thumbnail/test.jpg', $interpolatedString); 129 | } 130 | 131 | /** 132 | * Build a mock attachment object. 133 | * 134 | * @param \Codesleeve\Stapler\Interpolator 135 | * 136 | * @return \Codesleeve\Stapler\Attachment 137 | */ 138 | protected function build_mock_attachment($interpolator, $className = 'TestModel') 139 | { 140 | $instance = $this->build_mock_instance(); 141 | $attachmentConfig = new AttachmentConfig('photo', ['styles' => [], 'default_style' => 'original']); 142 | $imagine = m::mock('Imagine\Image\ImagineInterface'); 143 | $resizer = new \Codesleeve\Stapler\File\Image\Resizer($imagine); 144 | 145 | $attachment = m::mock('Codesleeve\Stapler\Attachment[getInstanceClass]', [$attachmentConfig, $interpolator, $resizer]); 146 | $attachment->shouldReceive('getInstanceClass')->andReturn($className); 147 | $attachment->setInstance($instance); 148 | 149 | return $attachment; 150 | } 151 | 152 | /** 153 | * Build a mock model instance. 154 | * 155 | * @return mixed 156 | */ 157 | protected function build_mock_instance() 158 | { 159 | $instance = m::mock('Codesleeve\Stapler\ORM\StaplerableInterface'); 160 | $instance->shouldReceive('getKey')->andReturn(1); 161 | $instance->shouldReceive('getAttribute')->with('photo_file_name')->andReturn('test.jpg'); 162 | $instance->shouldReceive('getAttribute')->with('photo_file_size')->andReturn(0); 163 | $instance->shouldReceive('getAttribute')->with('photo_content_type')->andReturn('image/jpeg'); 164 | 165 | return $instance; 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /tests/Codesleeve/Stapler/StaplerTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(defined('STAPLER_NULL')); 36 | } 37 | 38 | /** 39 | * Test that the Stapler class can build a single instance of 40 | * the Interpolator class. 41 | * 42 | * @test 43 | */ 44 | public function it_should_be_able_to_create_a_singleton_interpolator_instance() 45 | { 46 | $interpolator1 = Stapler::getInterpolatorInstance(); 47 | $interpolator2 = Stapler::getInterpolatorInstance(); 48 | 49 | $this->assertInstanceOf('Codesleeve\Stapler\Interfaces\Interpolator', $interpolator1); 50 | $this->assertSame($interpolator1, $interpolator2); 51 | } 52 | 53 | /** 54 | * Test that the Stapler class can build a single instance of 55 | * the Validator class. 56 | * 57 | * @test 58 | */ 59 | public function it_should_be_able_to_create_a_singleton_validator_instance() 60 | { 61 | $validator1 = Stapler::getValidatorInstance(); 62 | $validator2 = Stapler::getValidatorInstance(); 63 | 64 | $this->assertInstanceOf('Codesleeve\Stapler\Interfaces\Validator', $validator1); 65 | $this->assertSame($validator1, $validator2); 66 | } 67 | 68 | /** 69 | * Test that the Stapler class can build a single instance of 70 | * the Resizer class. 71 | * 72 | * @test 73 | */ 74 | public function it_should_be_able_to_create_a_singleton_resizer_instance() 75 | { 76 | $resizer1 = Stapler::getResizerInstance('Imagine\Gd\Imagine'); 77 | $resizer2 = Stapler::getResizerInstance('Imagine\Gd\Imagine'); 78 | 79 | $this->assertInstanceOf('Codesleeve\Stapler\Interfaces\Resizer', $resizer1); 80 | $this->assertSame($resizer1, $resizer2); 81 | } 82 | 83 | /** 84 | * Test that the Stapler class can build a single instance of 85 | * ImagineInterface. 86 | * 87 | * @test 88 | */ 89 | public function it_should_be_able_to_create_a_singleton_imagine_interface_instance() 90 | { 91 | $imagine1 = Stapler::getImagineInstance('Imagine\Gd\Imagine'); 92 | $imagine2 = Stapler::getImagineInstance('Imagine\Gd\Imagine'); 93 | 94 | $this->assertInstanceOf('Imagine\Image\ImagineInterface', $imagine1); 95 | $this->assertSame($imagine1, $imagine2); 96 | } 97 | 98 | /** 99 | * Test that the Stapler class can build a single instance of 100 | * Aws\S3\S3Client for each model/attachment combo. 101 | * 102 | * @test 103 | */ 104 | public function it_should_be_able_to_create_a_singleton_s3_client_instance_for_each_model_attachment_combo() 105 | { 106 | $dummyConfig = new AttachmentConfig('TestAttachment', [ 107 | 'styles' => [], 108 | 's3_client_config' => [ 109 | 'key' => '', 110 | 'secret' => '', 111 | 'region' => '', 112 | 'scheme' => 'http', 113 | ], 114 | ]); 115 | $mockAttachment = m::mock('Codesleeve\Stapler\Attachment')->makePartial(); 116 | $mockAttachment->shouldReceive('getInstanceClass')->twice()->andReturn('TestModel'); 117 | $mockAttachment->setConfig($dummyConfig); 118 | 119 | $s3Client1 = Stapler::getS3ClientInstance($mockAttachment); 120 | $s3Client2 = Stapler::getS3ClientInstance($mockAttachment); 121 | 122 | $this->assertInstanceOf('Aws\S3\S3Client', $s3Client1); 123 | $this->assertSame($s3Client1, $s3Client2); 124 | } 125 | 126 | /** 127 | * Test that the stapler class can build a single instance of 128 | * Codesleeve\Stapler\Config\NativeConfig. 129 | * 130 | * @test 131 | */ 132 | public function it_should_be_able_to_create_a_singleton_native_config_instance() 133 | { 134 | $config1 = Stapler::getConfigInstance(); 135 | $config2 = Stapler::getConfigInstance(); 136 | 137 | $this->assertInstanceOf('Codesleeve\Stapler\Config\NativeConfig', $config1); 138 | $this->assertInstanceOf('Codesleeve\Stapler\Interfaces\Config', $config1); 139 | $this->assertSame($config1, $config2); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /tests/Codesleeve/Stapler/StyleTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('foo', $styleOjbect->name); 33 | } 34 | 35 | /** 36 | * Test that the style class can accept a simple string value 37 | * of dimensions. 38 | * 39 | * @test 40 | */ 41 | public function it_should_be_able_to_accept_a_string_value() 42 | { 43 | $styleValue = '50x50'; 44 | 45 | $styleOjbect = new Style('foo', $styleValue); 46 | 47 | $this->assertEquals('50x50', $styleOjbect->dimensions); 48 | } 49 | 50 | /** 51 | * Test that the style class can accept an array of values to 52 | * parse. 53 | * 54 | * @test 55 | * 56 | * @param string $value 57 | */ 58 | public function it_should_be_able_to_accept_an_array_of_values($value = '') 59 | { 60 | $convertOptions = ['resolution-units' => 'ppi', 'resolution-x' => 300, 'resolution-y' => 300, 'jpeg_quality' => 100]; 61 | $styleValue = ['dimensions' => '50x50', 'auto_orient' => true, 'convert_options' => $convertOptions]; 62 | 63 | $styleOjbect = new Style('foo', $styleValue); 64 | 65 | $this->assertEquals('50x50', $styleOjbect->dimensions); 66 | $this->assertTrue($styleOjbect->autoOrient); 67 | $this->assertEquals($convertOptions, $styleOjbect->convertOptions); 68 | } 69 | 70 | /** 71 | * Test that the style class will throw an exception if passed an array 72 | * of values withou a 'dimensions' key. 73 | * 74 | * @test 75 | * @expectedException \Codesleeve\Stapler\Exceptions\InvalidStyleConfigurationException 76 | */ 77 | public function it_should_throw_an_exception_if_passed_an_array_of_values_withou_a_dimensions_key() 78 | { 79 | $convertOptions = ['resolution-units' => 'ppi', 'resolution-x' => 300, 'resolution-y' => 300, 'jpeg_quality' => 100]; 80 | $styleValue = ['auto_orient' => true, 'convert_options' => $convertOptions]; 81 | 82 | $styleOjbect = new Style('foo', $styleValue); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | add('Codesleeve\Stapler\\', __DIR__); 5 | 6 | date_default_timezone_set('UTC'); 7 | --------------------------------------------------------------------------------