Repository: siriusphp/upload
Branch: master
Commit: 219a10ec6806
Files: 48
Total size: 74.3 KB
Directory structure:
gitextract_5xugku7m/
├── .gitattributes
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── Bug.md
│ │ ├── Feature_Request.md
│ │ └── Question.md
│ └── workflows/
│ ├── ci.yml
│ └── docs.yml
├── .gitignore
├── .php_cs.cache
├── .scrutinizer.yml
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── autoload.php
├── composer.json
├── docs/
│ ├── cloud_upload.md
│ ├── couscous.yml
│ ├── custom_validation.md
│ ├── file_locking.md
│ ├── index.md
│ ├── installation.md
│ ├── integrations.md
│ ├── simple_example.md
│ ├── upload_aggregator.md
│ ├── upload_options.md
│ └── validation_rules.md
├── phpstan.neon
├── phpunit.xml
├── src/
│ ├── Container/
│ │ ├── ContainerInterface.php
│ │ └── Local.php
│ ├── Exception/
│ │ ├── InvalidContainerException.php
│ │ └── InvalidResultException.php
│ ├── Handler.php
│ ├── HandlerAggregate.php
│ ├── Result/
│ │ ├── Collection.php
│ │ ├── File.php
│ │ └── ResultInterface.php
│ ├── UploadHandlerInterface.php
│ └── Util/
│ └── Helper.php
└── tests/
├── .phpunit.result.cache
├── Pest.php
├── TestCase.php
├── phpunit.xml
├── phpunit_bootstrap.php
├── src/
│ ├── Container/
│ │ └── LocalTest.php
│ ├── HandlerAggregateTest.php
│ └── HandlerTest.php
└── web/
└── index.php
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitattributes
================================================
/docs export-ignore
/tests export-ignore
/.gitattributes export-ignore
/.gitignore export-ignore
/.scrutinizer.yml export-ignore
/.travis.yml export-ignore
/couscous.yml export-ignore
/index.md export-ignore
================================================
FILE: .github/ISSUE_TEMPLATE/Bug.md
================================================
---
name: 🐛 Bug
about: Did you encounter a bug?
---
### Bug Report
<!-- Fill in the relevant information below to help triage your issue. -->
| Q | A
|------------ | ------
| BC Break | yes/no
| Version | x.y.z
#### Summary
<!-- Provide a summary describing the problem you are experiencing. -->
#### How to reproduce
<!--
Provide steps to reproduce the issue.
If possible, also add a code snippet.
-->
================================================
FILE: .github/ISSUE_TEMPLATE/Feature_Request.md
================================================
---
name: 🎉 Feature Request
about: Do you have a new feature in mind?
---
### Feature Request
<!-- Fill in the relevant information below to help triage your issue. -->
| Q | A
|------------ | ------
| New Feature | yes/no
| BC Break | yes/no
#### Scenario / Use-case
<!-- Provide an explain in which scenario the feature would be helpful. -->
#### Summary
<!-- Provide a summary of the feature you would like to see implemented. -->
================================================
FILE: .github/ISSUE_TEMPLATE/Question.md
================================================
---
name: ❓ Question
about: Are you unsure about something?
---
### Question
<!-- Fill in the relevant information below to help triage your issue. -->
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
strategy:
matrix:
php: ['8.1', '8.2', '8.3']
include:
- php: '8.1'
send-to-scrutinizer: 'yes'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup PHP with fail-fast
uses: shivammathur/setup-php@v2
with:
send-to-scrutinizer: 'no'
phpunit-flags: '--no-coverage'
php-version: ${{ matrix.php }}
coverage: xdebug
env:
fail-fast: true
- name: Validate composer.json and composer.lock
run: composer validate
- name: Cache Composer packages
id: composer-cache
uses: actions/cache@v2
with:
path: vendor
key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-php-
- name: Install dependencies
if: steps.composer-cache.outputs.cache-hit != 'true'
run: composer install --prefer-dist --no-progress --no-suggest
- name: Run test suite
run: |
composer run stan
sudo composer run test
================================================
FILE: .github/workflows/docs.yml
================================================
name: Publish docs
on:
push:
tags:
- '*.*.*'
jobs:
docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Build docs
run: |
curl -OS https://couscous.io/couscous.phar
php couscous.phar generate --target=build/docs/ ./docs
- name: FTP Deployer
uses: sand4rt/ftp-deployer@v1.1
with:
host: ${{ secrets.DOCS_FTP_HOST }}
username: ${{ secrets.DOCS_FTP_USER }}
password: ${{ secrets.DOCS_FTP_PASSWORD }}
remote_folder: upload
# The local folder location
local_folder: build/docs/
# Remove existing files inside FTP remote folder
cleanup: false # optional
================================================
FILE: .gitignore
================================================
.idea/
.settings/
.buildpath
.project
build/
vendor/
composer.lock
atlassian-ide-plugin.xml
docs/couscous.phar
docs/.couscous/
couscous.phar
php-cs-fixer.phar
phpcbf.phar
phpcs.phar
================================================
FILE: .php_cs.cache
================================================
{"php":"7.2.4","version":"2.16.1","indent":" ","lineEnding":"\n","rules":{"blank_line_after_namespace":true,"braces":true,"class_definition":true,"constant_case":true,"elseif":true,"function_declaration":true,"indentation_type":true,"line_ending":true,"lowercase_keywords":true,"method_argument_space":{"on_multiline":"ensure_fully_multiline"},"no_break_comment":true,"no_closing_tag":true,"no_spaces_after_function_name":true,"no_spaces_inside_parenthesis":true,"no_trailing_whitespace":true,"no_trailing_whitespace_in_comment":true,"single_blank_line_at_eof":true,"single_class_element_per_statement":{"elements":["property"]},"single_import_per_statement":true,"single_line_after_imports":true,"switch_case_semicolon_to_colon":true,"switch_case_space":true,"visibility_required":true,"encoding":true,"full_opening_tag":true},"hashes":{"src\\Container\\ContainerInterface.php":1583588918,"src\\Container\\Local.php":71709479,"src\\Exception\\InvalidContainerException.php":1907413747,"src\\Exception\\InvalidResultException.php":466931389,"src\\Handler.php":534695839,"src\\HandlerAggregate.php":1499917949,"src\\Result\\Collection.php":2897468334,"src\\Result\\File.php":1434642790,"src\\UploadHandlerInterface.php":2299182802,"src\\Util\\Arr.php":1378696061,"src\\Result\\ResultInterface.php":844276353,"src\\Util\\Helper.php":3774982220}}
================================================
FILE: .scrutinizer.yml
================================================
filter:
paths: ["src/*"]
tools:
external_code_coverage: true
php_code_coverage: true
php_sim: true
php_mess_detector: true
php_pdepend: true
php_analyzer: true
php_cpd: true
================================================
FILE: .travis.yml
================================================
sudo: false
language: php
matrix:
include:
- php: 7.1
dist: bionic
env: DEPENDENCIES='low'
- php: 7.2
dist: bionic
env: DEPENDENCIES='low'
- php: 7.2
dist: bionic
- php: 7.3
dist: bionic
- php: 7.4
dist: bionic
- php: nightly
env: COMPOSER_FLAGS='--ignore-platform-reqs'
- php: nightly
env: COMPOSER_FLAGS='--ignore-platform-reqs' COMPOSER_FORCE='phpunit/phpunit ^9.0@dev'
fast_finish: true
allow_failures:
- php: 7.1
- php: nightly
before_script:
- composer self-update
- composer install --prefer-source
script:
- mkdir -p build/logs
- mkdir -p build/coverage
- vendor/bin/phpunit -c tests/phpunit.xml
- cd ../
after_script:
- wget https://scrutinizer-ci.com/ocular.phar
- if [ "$TRAVIS_PHP_VERSION" == "7.2" ]; then php ocular.phar code-coverage:upload --format=php-clover build/logs/clover.xml; fi
================================================
FILE: CHANGELOG.md
================================================
# CHANGELOG
## 2.0.0
- changed the `__constructor` parameters. Now you inject an optional `Sirius\Validation\ValueValidator` instance instead of an `Sirius\Validation\ErrorMessage` instance
- changed dependency to Sirius\Validation~2.0
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2014 Adrian Miu
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: README.md
================================================
# Sirius\Upload
[](https://github.com/siriusphp/upload)
[](https://github.com/siriusphp/upload/releases)
[](https://github.com/siriusphp/upload/blob/master/LICENSE)
[](https://github.com/siriusphp/upload/actions/workflows/ci.yml)
[](https://scrutinizer-ci.com/g/siriusphp/upload/code-structure)
[](https://scrutinizer-ci.com/g/siriusphp/upload)
[](https://packagist.org/packages/siriusphp/upload)
Framework agnostic upload handler library.
## Features
1. Validates files against usual rules: extension, file size, image size (wdith, height, ratio). It uses [Sirius Validation](https://github.com/siriusphp/validation) for this purpose.
2. Moves valid uploaded files into containers. Containers are usually local folders but you can implement your own or use other filesystem abstractions like [Gaufrette](https://github.com/KnpLabs/Gaufrette) or [Flysystem](https://github.com/FrenkyNet/Flysystem).
3. Works with PSR7 `UploadedFileInterface` objects and with Symfony's `UploadedFile`s (see [integrations](docs/integrations.md)).
Used by [Bolt CMS](https://bolt.cm/)
## Elevator pitch
```php
use Sirius\Upload\Handler as UploadHandler;
$uploadHandler = new UploadHandler('/path/to/local_folder');
// validation rules
$uploadHandler->addRule('extension', ['allowed' => ['jpg', 'jpeg', 'png']], '{label} should be a valid image (jpg, jpeg, png)', 'Profile picture');
$uploadHandler->addRule('size', ['max' => '20M'], '{label} should have less than {max}', 'Profile picture');
$result = $uploadHandler->process($_FILES['picture']); // ex: subdirectory/my_headshot.png
if ($result->isValid()) {
// do something with the image like attaching it to a model etc
try {
$profile->picture = $result->name;
$profile->save();
$result->confirm(); // this will remove the .lock file
} catch (\Exception $e) {
// something wrong happened, we don't need the uploaded files anymore
$result->clear();
throw $e;
}
} else {
// image was not moved to the container, where are error messages
$messages = $result->getMessages();
}
```
## Links
- [documentation](https://www.sirius.ro/php/sirius/upload/)
- [changelog](CHANGELOG.md)
================================================
FILE: autoload.php
================================================
<?php
spl_autoload_register(function ($class) {
// what namespace prefix should be recognized?
$prefix = 'Sirius\Upload\\';
// does the requested class match the namespace prefix?
$prefix_len = strlen($prefix);
if (substr($class, 0, $prefix_len) !== $prefix) {
return;
}
// strip the prefix off the class
$class = substr($class, $prefix_len);
// a partial filename
$part = str_replace('\\', DIRECTORY_SEPARATOR, $class) . '.php';
// directories where we can find classes
$dirs = array(
__DIR__ . DIRECTORY_SEPARATOR . 'src',
__DIR__ . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'src',
);
// go through the directories to find classes
foreach ($dirs as $dir) {
$file = $dir . DIRECTORY_SEPARATOR . $part;
if (is_readable($file)) {
require $file;
return;
}
}
});
================================================
FILE: composer.json
================================================
{
"name": "siriusphp/upload",
"description": "Framework agnostic upload library",
"type": "library",
"license": "MIT",
"keywords": [
"form",
"upload",
"validation",
"file",
"file upload",
"security",
"psr-7"
],
"authors": [
{
"name": "Adrian Miu",
"email": "adrian@adrianmiu.ro"
}
],
"require": {
"php": ">=8.1",
"siriusphp/validation": "^4.0"
},
"require-dev": {
"laminas/laminas-diactoros": "^3.3",
"symfony/http-foundation": "^6.3",
"pestphp/pest": "^2.24",
"pestphp/pest-plugin-drift": "^2.5",
"symfony/mime": "^6.3",
"phpstan/phpstan": "^1.10"
},
"suggest": {
"league/flysystem": "To upload to different destinations, not just to the local file system",
"knplabs/gaufrette": "Alternative filesystem abstraction library for upload destinations"
},
"autoload": {
"psr-4": {
"Sirius\\Upload\\": "src/"
}
},
"scripts": {
"stan": [
"php vendor/bin/phpstan analyse"
],
"csfix": [
"tools/php-cs-fixer/vendor/bin/php-cs-fixer fix --standard=PSR-2 src"
],
"test": [
"php vendor/bin/pest"
],
"build-docs": [
"php couscous.phar generate --target=build/docs/ ./docs"
],
"docs": [
"cd docs && php ../couscous.phar preview"
]
},
"config": {
"allow-plugins": {
"pestphp/pest-plugin": true
}
}
}
================================================
FILE: docs/cloud_upload.md
================================================
---
title: Upload into the cloud | Sirius Upload
---
# Upload into the cloud
If you want to store uploaded files in different locations your containers must implement the `Sirius\Upload\Container\ContainerInterface`.
The example below is not based on real-life code, it's for illustration purposes only.
```php
$amazonBucket = new AmazonBucket();
$container = new AmazonContainer($amazonBucket);
$uploadHandler = new UploadHandler($container);
```
You can easily create upload containers on top of [Gaufrette](https://github.com/KnpLabs/Gaufrette) or [Flysystem](https://github.com/FrenkyNet/Flysystem).
================================================
FILE: docs/couscous.yml
================================================
template:
url: https://github.com/siriusphp/Template-ReadTheDocs
# List of directories to exclude from the processing (default contains "vendor" and "website")
# Paths are relative to the repository root
exclude:
- website
- vendor
- test
- src
- build
# Base URL of the published website (no "/" at the end!)
# You are advised to set and use this variable to write your links in the HTML layouts
baseUrl: https://www.sirius.ro/php/sirius/upload
paypal: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=SGXDKNJCXFPJU
gacode: UA-535999-18
projectName: Sirius\Upload
title: Sirius\Upload
subTitle: Framework agnostic upload library
# The left menu bar
menu:
sections:
# versions:
# name: Versions
# items:
# two:
# text: "2.0"
# relativeUrl:
# one:
# text: "1.0"
# relativeUrl: 1_0/
guide:
name: Getting started
items:
getting_started:
text: Introduction
relativeUrl:
installation:
text: Installation
relativeUrl: installation.html
simple_example:
text: Simple example
relativeUrl: simple_example.html
validators:
text: Validation rules
relativeUrl: validation_rules.html
aggregator:
text: Upload aggregator
relativeUrl: upload_aggregator.html
advanced:
name: Advanced topics
items:
file_locking:
text: File locking
relativeUrl: file_locking.html
options:
text: Upload options
relativeUrl: upload_options.html
cookbook:
name: Cookbook
items:
cloud_upload:
text: Uploads into the cloud
relativeUrl: cloud_upload.html
custom_validation:
text: Custom validation
relativeUrl: custom_validation.html
integrations:
text: Integrations
relativeUrl: integrations.html
================================================
FILE: docs/custom_validation.md
================================================
---
title: Custom upload validation | Sirius Upload
---
# Custom validation
If you need implement specific file upload validation rules remember to look at the documentation for the [Sirius\Validation](https://www.gihub.com/siriusphp/validation) library.
For example if you have a system where the users have upload quotas and you want to make sure that they don't exceed their allocated quota you ca do the following
```php
function check_user_quota($file) {
return User::instance()->getRemainingQuota() < $file['size'];
}
$uploadHandler->addRule('callback', array('callback' => 'check_user_quota'), 'Sorry, but you don\'t have enough space to upload this file');
```
# Use a custom validator
You can inject a `Sirius\Validation\ValueValidator` upon construction. The example below assumes there is a dependency injection container.
```php
$ruleFactory = $container->get('Sirius\Validation\RuleFactory');
$ruleFactory->register('quota_is_met', 'MyApp\Validation\Rule\FileQuota');
$valueValidator = $container->get('Sirius\Validation\ValueValidator');
$handler = new Sirius\Upload\Handler('/path/to/dir', array(), $valueValidator);
$handler->addRule('quota_is_met');
```
================================================
FILE: docs/file_locking.md
================================================
---
title: File locking during uploads | Sirius Upload
---
# What is 'file locking'?
Usually, an application accepts file uploads to store them for future use (product images, people resumes etc).
But from the time an uploaded file is moved to its container (the folder on disk, an S3 bucket) until the actual data is saved there are things that can go wrong (eg: the database goes down and the uploaded image cannot be attached to a model).
The `locking` functionality was implemented for this reason. Whenever a file is uploaded, on the same location another file with the `.lock` extension is created. This file is removed when the upload is confirmed.
Worst case scenario (when the system breaks down so you cannot even execute the `clear()` method) you will be able to look into the upload container (local directory, S3 bucket) and "spot" the unused files.
If you want to take advantage of this feature you are **REQUIRED** use `confirm()` or you will end up with `.lock` files everywhere.
If you don't like it, use `$uploadHandler->setAutoconfirm(true)` and all uploaded files will automatically confirmed.
================================================
FILE: docs/index.md
================================================
[](https://github.com/siriusphp/upload)
[](https://github.com/siriusphp/upload/releases)
[](https://github.com/siriusphp/upload/blob/master/LICENSE)
[](https://travis-ci.org/siriusphp/upload)
[](https://travis-ci.org/siriusphp/upload)
[](https://scrutinizer-ci.com/g/siriusphp/upload/code-structure)
[](https://scrutinizer-ci.com/g/siriusphp/upload)
[](https://packagist.org/packages/siriusphp/upload)
# Sirius Upload
This is a framework agnostic upload handler library that is flexible and easy to use.
## Features
1. Validates files against usual rules: extension, file size, image size (width, height, ratio). It uses [Sirius Validation](https://github.com/siriusphp/validation) for this purpose.
2. Moves valid uploads into containers. Containers are usually local folders but you can implement your own or use other filesystem abstractions like [Gaufrette](https://github.com/KnpLabs/Gaufrette) or [Flysystem](https://github.com/FrenkyNet/Flysystem).
3. Works with PSR7 `UploadedFileInterface` objects and with Symfony's `UploadedFile`s (see [integrations](integrations.md)).
Used by [Bolt CMS](https://bolt.cm/)
## How it works
1. Uploaded file is validated against the rules. By default the library will check if the upload is valid (ie: no errors during upload)
2. The name of the uploaded file is sanitized (keep only letters, numbers and underscore). You may implement your own sanitization function if you want.
3. If overwrite is not allowed, and a file with the same name already exists in the container, the library will prepend the timestamp to the filename.
4. Moves the uploaded file to the container. It also create a lock file (filename + '.lock') so that we know the upload is not confirmed. See [file locking](file_locking.md)
5. If something wrong happens in your app and you want to get rid of the uploaded file you can `clear()` the uploaded file which will remove the file and its `.lock` file. Only files that have a corresponding `.lock` file attached can be cleared
6. If everything is in order you can `confirm` the upload. This will remove the `.lock` file attached to the upload file.
## Important notes
##### 1. The library makes no assumptions about the "web availability" of the uploaded file.
Most of the times once you have a valid upload the new file will be reachable on the internet. You may upload your files to `/var/www/public/images/users/` and have the files accessible at `//cdn.domain.com/users/`. It's up to you to make your app work with the result of the upload.
##### 2. You can handle multiple uploads at once if they have the same name
If you upload multiple files with the same name (eg: `<input type="file" name="pictures[]">`) but you have to keep in mind that the `process()` and `getMessages()` methods will return arrays
```php
$result = $uploadHandler->process($_FILES['pictures']);
// will return a collection of files which implements \Iterator interface
$messages = $result->getMessages();
// may return if the second file is not valid
array(
'1' => 'File type not accepted'
);
```
In this case the library normalizes the `$_FILES` array as PHP messes up the upload array.
It is up to you to decide what you want to do when some files fail to upload (eg: keep the valid files and discard the failed image or display error messages for the invalid images)
================================================
FILE: docs/installation.md
================================================
---
title: Installing Sirius\Upload | Sirius Upload
---
#Installation
## Using composer
Sirius\Validation is available on [Packagist](https://packagist.org/packages/siriusphp/upload) so you can use
```
composer require siriusphp/upload
```
Make sure to include the Composer autoload file in your project
```php
require 'vendor/autoload.php';
```
## Downloading a `.zip` file
This project is also available for download as a `.zip` file on GitHub. Visit the [releases page](https://github.com/siriusphp/upload/releases), select the version you want, and click the "Source code (zip)" download button.
================================================
FILE: docs/integrations.md
================================================
---
title: Integrations | Sirius Upload
---
# Integrations with other libraries
## PSR-7's UploadedFileInterface
If you are using a library like [Laminas Diactoros](https://github.com/laminas/laminas-diactoros) that can provide an array of objects that implement `UploadedFileInterface` from the PSR-7 standard you can do the following
```php
/** @var Sirius\Upload\Handler $uploadHandler */
/** @var Laminas\Diactoros\ServerRequest $request */
$result = $uploadHandler->process($request->getUploadedFiles());
```
## Symfony's UploadedFile
If you integrate this into a project that uses Symfony's HTTP Foundation component you can do the following:
```php
/** @var Sirius\Upload\Handler $uploadHandler */
/** @var Symfony\Component\HttpFoundation\Request $request */
$result = $uploadHandler->process($request->files->all());
```
================================================
FILE: docs/simple_example.md
================================================
---
title: Simple upload example | Sirius Upload
---
# Simple example
### Initialize the upload handler class
Let's consider a simple contact form that has the following field: `name`, `email`, `phone` and `message`.
```php
use Sirius\Upload\Handler as UploadHandler;
$uploadHandler = new UploadHandler('/path/to/local_folder');
// set up the validation rules
$uploadHandler->addRule('extension', ['allowed' => 'jpg', 'jpeg', 'png'], '{label} should be a valid image (jpg, jpeg, png)', 'Profile picture');
$uploadHandler->addRule('size', ['size' => '20M'], '{label} should be less than {size}', 'Profile picture');
$uploadHandler->addRule('imageratio', ['ratio' => 1], '{label} should be a square image', 'Profile picture');
```
### Process the upload
```php
$result = $uploadHandler->process($_FILES['picture']); // ex: subdirectory/my_headshot.png
if ($result->isValid()) {
try {
// do something with the image like attaching it to a model etc
$profile->picture = $result->name;
$profile->save();
$result->confirm(); // this will remove the .lock file
} catch (\Exception $e) {
// something wrong happened, we don't need the uploaded files anymore
$result->clear();
throw $e;
}
} else {
// image was not moved to the container, where are error messages
$messages = $result->getMessages();
}
```
================================================
FILE: docs/upload_aggregator.md
================================================
---
title: The upload aggregator | Sirius Upload
---
# The upload aggregator
Sometimes your form may upload multiple files to the server. To reduce the number of `process()`, `clear()` and `confirm()` calls you can use an "upload handler aggregate"
```php
use Sirius\Upload\HandlerAggregate as UploadHandlerAggregate;
$uploadHandlerAggregate = new UploadHandlerAggregate();
$uploadHandlerAggregate->addHandler('picture', $previouslyCreatedUploadHandlerForTheProfilePicture);
$uploadHandlerAggregate->addHandler('resume', $previouslyCreatedUploadHandlerForTheResume);
$result = $uploadHandlerAggregate->process($_FILES);
if ($result->isValid()) {
// do something with the image like attaching it to a model etc
try {
$profile->picture = $result['picture']->name;
$profile->resume = $result['resume']->name;
$profile->save();
$result->confirm(); // this will remove the .lock files
} catch (\Exception $e) {
// something wrong happened, we don't need the uploaded files anymore
$result->clear();
throw $e;
}
} else {
// image was not moved to the container, where are error messages
$messages = $result->getMessages();
}
```
You can see the aggregator and handlers in action in the [tests/web/index.php](https://www.github.com/siriusphp/upload/blob/master/tests/web/index.php)
================================================
FILE: docs/upload_options.md
================================================
---
title: Upload options | Sirius Upload
---
#Upload options
There are a few options you can choose to use while using the Sirius\Upload library
## Configure the uploader upon construction
```php
user \Sirius\Upload\Handler;
$uploadHandler = Handler('/path/to/dir', array(
Handler::OPTION_AUTOCONFIRM => true,
Handler::OPTION_OVERWRITE => true,
Handler::OPTION_PREFIX => '/subdirectory/' . time() . '_',
));
```
## Set options during execution
#### Overwrite existing files
A file is saved into the destination folder under it's own name. And there is a chance a file with that name might already be there.
You can choose to overwrite the existing file if you want. The library doesn't overwrite files by default.
```php
$uploadHandler->setOverwrite(true);
```
#### Auto-confirm uploads
As explained in the "[file locking](file_locking.md)" section, the uploaded files are `locked` and you have to manually `confirm()` the uploads to unlock them.
You can override this default behaviour via:
```php
$uploadHandler->setAutoconfirm(false);
```
#### Prefixing uploads
Sometimes you want to set up a prefix for your uploaded files (which can be a subdirectory, a timestamp etc). You can do this via:
```php
$uploadHandler->setPrefix('subdirectory/append_');
```
You can use a function/callback as the prefix
```php
function upload_prefix($file_name) {
return substr(md5($file_name), 0, 5) . '/';
}
$uploadHandler->setPrefix('upload_prefix');
```
## Filename sanitization
By default the library cleans up the name of the uploaded file by preserving only letters and numbers. If you want something else you set up a sanitizer callback:
```php
$uploadHandler->setSanitizerCallback(function($name){
return mktime() . preg_replace('/[^a-z0-9\.]+/', '-', strtolower($name));
});
```
================================================
FILE: docs/validation_rules.md
================================================
---
title: File upload validation rules | Sirius Upload
---
# Upload validation rules
For validating the uploads the library uses the [Sirius\Validation](https://www.sirius.ro/php/sirius/validation) library.
You add validation rules using the following command:
```php
$uploadHandler->addRule($ruleName, $ruleOptions, $errorMessage, $fieldLabel);
```
The following validation rules are available.
### Upload validators
#### extension
```php
$uploadHandler->addRule('extension', ['allowed' => 'doc,pdf']);
// or any other format that is understandable by the Sirius\Validation library, like
$uploadHandler->addRule('extension', 'allowed=doc,pdf', '{label} should be a DOC or PDF file', 'The resume');
```
#### image
```php
$uploadHandler->addRule('image', 'allowed=jpg,png');
```
#### size
The `size` option can be a number or a string like '10K', '0.5M' or '1.3G` (default: 2M)
```php
$uploadHandler->addRule('size', 'size=2M');
```
#### imagewidth
The options `min` and `max` are presented in pixels
```php
$uploadHandler->addRule('imagewidth', 'min=100&max=2000');
```
#### imageheight
The options `min` and `max` are presented in pixels
```php
$uploadHandler->addRule('imageheight', 'min=100&max=2000');
```
#### imageratio
The option `ratio` can be a number (eg: 1.3) or a ratio-like string (eg: 4:3, 16:9).
The option `error_margin` specifies how much the image is allowed to deviate from the target ratio. Default value is 0
```php
$uploadHandler->addRule('imageratio', 'ratio=4:3&error_margin=0.01');
```
*Note!* The upload validators use only the `tmp_name` and `name` values to perform the validation
================================================
FILE: phpstan.neon
================================================
parameters:
level: 8
checkGenericClassInNonGenericObjectType: false
paths:
- src
================================================
FILE: phpunit.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.3/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
>
<testsuites>
<testsuite name="Test Suite">
<directory suffix="Test.php">./tests</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory suffix=".php">./app</directory>
<directory suffix=".php">./src</directory>
</include>
</source>
</phpunit>
================================================
FILE: src/Container/ContainerInterface.php
================================================
<?php
declare(strict_types=1);
namespace Sirius\Upload\Container;
interface ContainerInterface
{
/**
* Check if the container is writable
*/
public function isWritable(): bool;
/**
* This will check if a file is in the container
*/
public function has(string $file): bool;
/**
* Saves the $content string as a file
*
* @param string $file
* @param string $content
*/
public function save(string $file, string $content): bool;
/**
* Delete the file from the container
*/
public function delete(string $file): bool;
/**
* Moves a temporary uploaded file to a destination in the container
*/
public function moveUploadedFile(string $localFile, string $destination): bool;
}
================================================
FILE: src/Container/Local.php
================================================
<?php
declare(strict_types=1);
namespace Sirius\Upload\Container;
class Local implements ContainerInterface
{
protected string $baseDirectory;
public function __construct(string $baseDirectory)
{
$this->baseDirectory = $this->normalizePath($baseDirectory) . DIRECTORY_SEPARATOR;
$this->ensureDirectory($this->baseDirectory);
}
protected function normalizePath(string $path): string
{
$path = dirname(rtrim($path, '\\/') . DIRECTORY_SEPARATOR . 'xxx');
return rtrim($path, DIRECTORY_SEPARATOR);
}
protected function ensureDirectory(string $directory): bool
{
if ( ! is_dir($directory)) {
mkdir($directory, 0755, true);
}
return is_dir($directory) && $this->isWritable();
}
/**
* Check if the container is writable
*/
public function isWritable(): bool
{
return is_writable($this->baseDirectory);
}
/**
* This will check if a file is in the container
*
* @param string $file
*
* @return bool
*/
public function has(string $file): bool
{
return $file && file_exists($this->baseDirectory . $file);
}
public function save(string $file, string $content): bool
{
$file = $this->normalizePath($file);
$dir = dirname($this->baseDirectory . $file);
if ($this->ensureDirectory($dir)) {
return (bool)file_put_contents($this->baseDirectory . $file, $content);
}
return false;
}
public function delete(string $file): bool
{
$file = $this->normalizePath($file);
if (file_exists($this->baseDirectory . $file)) {
return unlink($this->baseDirectory . $file);
}
return true;
}
public function moveUploadedFile(string $localFile, string $destination): bool
{
$dir = dirname($this->baseDirectory . $destination);
if (file_exists($localFile) && $this->ensureDirectory($dir)) {
if (is_readable($localFile)) {
// rename() would be good but this is better because $localFile may become 'unwritable'
$result = copy($localFile, $this->baseDirectory . $destination);
@unlink($localFile);
return $result;
}
}
return false;
}
}
================================================
FILE: src/Exception/InvalidContainerException.php
================================================
<?php
declare(strict_types=1);
namespace Sirius\Upload\Exception;
class InvalidContainerException extends \RuntimeException
{
}
================================================
FILE: src/Exception/InvalidResultException.php
================================================
<?php
declare(strict_types=1);
namespace Sirius\Upload\Exception;
class InvalidResultException extends \RuntimeException
{
}
================================================
FILE: src/Handler.php
================================================
<?php
declare(strict_types=1);
namespace Sirius\Upload;
use Sirius\Upload\Container\ContainerInterface;
use Sirius\Upload\Container\Local as LocalContainer;
use Sirius\Upload\Exception\InvalidContainerException;
use Sirius\Upload\Result\ResultInterface;
use Sirius\Upload\Util\Helper;
use Sirius\Validation\ValueValidator;
class Handler implements UploadHandlerInterface
{
// constants for constructor options
const OPTION_PREFIX = 'prefix';
const OPTION_OVERWRITE = 'overwrite';
const OPTION_AUTOCONFIRM = 'autoconfirm';
// constants for validation rules
const RULE_EXTENSION = 'extension';
const RULE_SIZE = 'size';
const RULE_IMAGE = 'image';
const RULE_IMAGE_HEIGHT = 'imageheight';
const RULE_IMAGE_WIDTH = 'imagewidth';
const RULE_IMAGE_RATIO = 'imageratio';
protected ContainerInterface $container;
/**
* Prefix to be added to the file.
* It can be a subfolder (if it ends with '/', a string to be used as prefix)
* or a callback that returns a string
*
* @var string|callable
*/
protected mixed $prefix = '';
/**
* When uploading a file that has the same name as a file that is
* already in the container should it overwrite it or use another name
*/
protected bool $overwrite = false;
/**
* Whether or not the uploaded files are auto confirmed
*/
protected bool $autoconfirm = false;
protected ?ValueValidator $validator = null;
/**
* @var callable
*/
protected mixed $sanitizerCallback = null;
/**
* @param string|ContainerInterface $directoryOrContainer
* @param array<string, mixed> $options
*
* @throws InvalidContainerException
*/
public function __construct(mixed $directoryOrContainer, array $options = [], ValueValidator $validator = null)
{
$container = $directoryOrContainer;
if (is_string($directoryOrContainer)) {
$container = new LocalContainer($directoryOrContainer);
}
if ( ! $container instanceof ContainerInterface) {
throw new InvalidContainerException('Destination container for uploaded files is not valid');
}
$this->container = $container;
// create the validator
if ( ! $validator) {
$validator = new ValueValidator();
}
$this->validator = $validator;
// set options
$availableOptions = [
static::OPTION_PREFIX => 'setPrefix',
static::OPTION_OVERWRITE => 'setOverwrite',
static::OPTION_AUTOCONFIRM => 'setAutoconfirm'
];
foreach ($availableOptions as $key => $method) {
if (isset($options[$key])) {
$this->{$method}($options[$key]);
}
}
}
public function setOverwrite(bool $overwrite): self
{
$this->overwrite = (bool)$overwrite;
return $this;
}
/**
* File prefix for the upload. Can be
* - a folder (if it ends with /)
* - a string to be used as prefix
* - a function that returns a string
*
* @param string|callable $prefix
*/
public function setPrefix(mixed $prefix): self
{
$this->prefix = $prefix;
return $this;
}
/**
* Enable/disable upload autoconfirmation
* Autoconfirmation does not require calling `confirm()`
*/
public function setAutoconfirm(bool $autoconfirm): self
{
$this->autoconfirm = (bool)$autoconfirm;
return $this;
}
/**
* Set the sanitizer function for cleaning up the file names
* @throws \InvalidArgumentException
*/
public function setSanitizerCallback(callable|\Closure $callback): self
{
if ( ! is_callable($callback)) {
throw new \InvalidArgumentException('The $callback parameter is not a valid callable entity');
}
$this->sanitizerCallback = $callback;
return $this;
}
/**
* Add validation rule (extension|size|width|height|ratio)
*
* @param array<string, mixed> $options
*/
public function addRule(string $name, array $options = [], string $errorMessageTemplate = null, string $label = null): self
{
$predefinedRules = [
static::RULE_EXTENSION,
static::RULE_IMAGE,
static::RULE_SIZE,
static::RULE_IMAGE_WIDTH,
static::RULE_IMAGE_HEIGHT,
static::RULE_IMAGE_RATIO
];
// convert to a name that is known by the default RuleFactory
if (in_array($name, $predefinedRules)) {
$name = 'upload' . $name;
}
if ($this->validator) {
$this->validator->add($name, $options, $errorMessageTemplate, $label);
}
return $this;
}
/**
* Processes a file upload and returns an upload result file/collection
* @return Result\Collection|Result\File|ResultInterface
*/
public function process(mixed $files): ResultInterface
{
$files = Helper::normalizeFiles($files);
foreach ($files as $k => $file) {
$files[$k] = $this->processSingleFile($file);
}
if (count($files) == 1) {
return new Result\File(array_pop($files), $this->container);
}
return new Result\Collection($files, $this->container);
}
/**
* Processes a single uploaded file
* - sanitize the name
* - validates the file
* - if valid, moves the file to the container
*
* @param array<string, mixed> $file
*
* @return array<string, mixed>
*/
protected function processSingleFile(array $file): array
{
// store it for future reference
$file['original_name'] = $file['name'];
// sanitize the file name
$file['name'] = $this->sanitizeFileName($file['name']);
$file = $this->validateFile($file);
// if there are messages the file is not valid
if (isset($file['messages']) && $file['messages']) {
return $file;
}
// add the prefix
$prefix = '';
if (is_callable($this->prefix)) {
$prefix = (string)call_user_func($this->prefix, $file['name']);
} elseif (is_string($this->prefix)) {
$prefix = (string)$this->prefix;
}
// if overwrite is not allowed, check if the file is already in the container
if ( ! $this->overwrite) {
if ($this->container->has($prefix . $file['name'])) {
// add the timestamp to ensure the file is unique
// method is not bulletproof but it's pretty safe
$file['name'] = time() . '_' . $file['name'];
}
}
// attempt to move the uploaded file into the container
if ( ! $this->container->moveUploadedFile($file['tmp_name'], $prefix . $file['name'])) {
$file['name'] = false;
return $file;
}
$file['name'] = $prefix . $file['name'];
// create the lock file if autoconfirm is disabled
if ( ! $this->autoconfirm) {
$this->container->save($file['name'] . '.lock', (string)time());
}
return $file;
}
/**
* Validates a file according to the rules configured on the handler
*
* @param array<string, mixed> $file
*
* @return array<string, mixed>
*/
protected function validateFile(array $file): array
{
if ($this->validator && ! $this->validator->validate($file)) {
$file['messages'] = $this->validator->getMessages();
}
return $file;
}
/**
* Sanitize the name of the uploaded file by stripping away bad characters
* and replacing "invalid" characters with underscore _
*/
protected function sanitizeFileName(string $name): string
{
if (is_callable($this->sanitizerCallback)) {
return call_user_func($this->sanitizerCallback, $name);
}
return preg_replace('/[^A-Za-z0-9\.]+/', '_', $name); // @phpstan-ignore-line
}
}
================================================
FILE: src/HandlerAggregate.php
================================================
<?php
declare(strict_types=1);
namespace Sirius\Upload;
use Sirius\Upload\Result\Collection;
use Sirius\Validation\Util\Arr;
class HandlerAggregate implements \IteratorAggregate
{
/**
* @var array<string, Handler> $handlers
*/
protected array $handlers = [];
/**
* Adds a handler on the aggregate
*/
public function addHandler(string $selector, Handler $handler): self
{
$this->handlers[$selector] = $handler;
return $this;
}
/**
* @param array<string, mixed> $files
*
* @return Result\Collection
*/
public function process(mixed $files): mixed
{
$result = new Collection();
foreach ($this->handlers as $selector => $handler) {
/* @var $handler Handler */
$selectedFiles = Arr::getBySelector($files, $selector);
if (empty($selectedFiles)) {
continue;
}
foreach ($selectedFiles as $path => $file) {
if (is_array($file)) {
$result[$path] = $handler->process($file);
}
}
}
return $result;
}
public function getIterator(): \Traversable
{
return new \ArrayIterator($this->handlers);
}
}
================================================
FILE: src/Result/Collection.php
================================================
<?php
declare(strict_types=1);
namespace Sirius\Upload\Result;
use Sirius\Upload\Container\ContainerInterface;
class Collection extends \ArrayIterator implements ResultInterface
{
/**
* @param array<int, mixed> $files
*/
public function __construct(array $files = [], ContainerInterface $container = null)
{
$filesArray = [];
if ($container && ! empty($files)) {
foreach ($files as $key => $file) {
$filesArray[$key] = new File($file, $container);
}
}
parent::__construct($filesArray);
}
public function clear(): void
{
foreach ($this as $file) {
/* @var $file \Sirius\Upload\Result\File */
$file->clear();
}
}
public function confirm(): void
{
foreach ($this as $file) {
/* @var $file \Sirius\Upload\Result\File */
$file->confirm();
}
}
public function isValid(): bool
{
foreach ($this->getMessages() as $messages) {
if ($messages) {
return false;
}
}
return true;
}
/**
* @return array<string, array<string, mixed>>
*/
public function getMessages(): array
{
$messages = [];
foreach ($this as $key => $file) {
/* @var $file \Sirius\Upload\Result\File */
$messages[$key] = $file->getMessages();
}
return $messages;
}
}
================================================
FILE: src/Result/File.php
================================================
<?php
declare(strict_types=1);
namespace Sirius\Upload\Result;
use Sirius\Upload\Container\ContainerInterface;
/**
* @property string $name
*/
class File implements ResultInterface
{
/**
* Array containing the details of the uploaded file:
* - name (uploaded name)
* - original name
* - tmp_name
* etc
*
* @var array<string, mixed>
*/
protected array $file;
/**
* The container to which this file belongs to
*/
protected ContainerInterface $container;
/**
* @param array<string, mixed> $file
* @param ContainerInterface $container
*/
public function __construct(array $file, ContainerInterface $container)
{
$this->file = $file;
$this->container = $container;
}
/**
* Returns if the uploaded file is valid
*
* @return bool
*/
public function isValid(): bool
{
return $this->file['name'] && count($this->getMessages()) === 0;
}
/**
* Returns the validation error messages
*
* @return array<string, mixed>
*/
public function getMessages(): array
{
if (isset($this->file['messages'])) {
return $this->file['messages'];
} else {
return [];
}
}
/**
* The file that was saved during process() and has a .lock file attached
* will be cleared, in case the form processing fails
*/
public function clear(): void
{
$this->container->delete($this->name);
$this->container->delete($this->name . '.lock');
$this->file['name'] = null;
}
/**
* Remove the .lock file attached to the file that was saved during process()
* This should happen if the form fails validation/processing
*/
public function confirm(): void
{
$this->container->delete($this->name . '.lock');
}
public function __get(string $name): mixed
{
if (isset($this->file[$name])) {
return $this->file[$name];
}
return null;
}
}
================================================
FILE: src/Result/ResultInterface.php
================================================
<?php
declare(strict_types=1);
namespace Sirius\Upload\Result;
interface ResultInterface
{
/**
* Returns if the uploaded file is valid
*
* @return bool
*/
public function isValid(): bool;
/**
* Returns the validation error messages
*
* @return array<string, mixed>
*/
public function getMessages(): array;
/**
* The file that was saved during process() and has a .lock file attached
* will be cleared, in case the form processing fails
*/
public function clear(): void;
/**
* Remove the .lock file attached to the file that was saved during process()
* This should happen if the form fails validation/processing
*/
public function confirm(): void;
}
================================================
FILE: src/UploadHandlerInterface.php
================================================
<?php
declare(strict_types=1);
namespace Sirius\Upload;
use Psr\Http\Message\UploadedFileInterface;
use Sirius\Upload\Result\ResultInterface;
use Symfony\Component\HttpFoundation\File\UploadedFile;
interface UploadHandlerInterface
{
/**
* This function will process the files received from $_FILES,
* validate them and save them into the container.
*
* Along with the file saved into the container a .lock file should
* be added by the container save() method so, in case the form is
* not validated, the uploaded file will be removed.
*
* @param array<string, mixed>|UploadedFileInterface|UploadedFile $files
*
* @return Result\Collection|Result\File|ResultInterface
*/
public function process(mixed $files): mixed;
}
================================================
FILE: src/Util/Helper.php
================================================
<?php
declare(strict_types=1);
namespace Sirius\Upload\Util;
use Psr\Http\Message\UploadedFileInterface;
use Symfony\Component\HttpFoundation\File\UploadedFile;
class Helper
{
const PSR7_UPLOADED_FILE_CLASS = '\Psr\Http\Message\UploadedFileInterface';
const SYMFONY_UPLOADED_FILE_CLASS = 'Symfony\Component\HttpFoundation\File\UploadedFile';
/**
* We do not type-hint or import the class since it may not be used
* @return array<string, mixed>
*/
public static function extractFromUploadedFileInterface(UploadedFileInterface $file): array
{
$tempName = tempnam(sys_get_temp_dir(), 'srsupld_');
if (!$tempName) {
throw new \RuntimeException('Could not create temporary directory');
}
$file->moveTo($tempName);
$result = [
'name' => $file->getClientFilename(),
'tmp_name' => $tempName,
'type' => $file->getClientMediaType(),
'error' => $file->getError(),
'size' => $file->getSize()
];
return $result;
}
/**
* @return array<string, mixed>
*/
public static function extractFromSymfonyFile(UploadedFile $file): array
{
$result = [
'name' => $file->getClientOriginalName(),
'tmp_name' => $file->getPathname(),
'type' => $file->getMimeType(),
'error' => $file->getError(),
'size' => $file->getSize()
];
return $result;
}
/**
* @param array<string, mixed> $files
*
* @return array<string|int, mixed>
*/
public static function remapFilesArray(array $files): array
{
$result = [];
foreach (array_keys($files['name']) as $k) {
$result[$k] = [
'name' => $files['name'][$k],
'type' => $files['type'][$k] ?? null,
'size' => $files['size'][$k] ?? null,
'error' => $files['error'][$k] ?? null,
'tmp_name' => $files['tmp_name'][$k] ?? null
];
}
return $result;
}
/**
* Fixes the $_FILES array problem and ensures the result is an array of files
*
* PHP's $_FILES variable is not properly formatted for iteration when
* multiple files are uploaded under the same name
* @see https://www.php.net/manual/en/features.file-upload.php
*
* @param array<string, mixed|UploadedFile|UploadedFileInterface>|UploadedFile|UploadedFileInterface $files
*
* @return array<int, mixed>
*/
public static function normalizeFiles(mixed $files): array
{
if (empty($files)) {
return [];
}
if (is_object($files)) {
if (is_subclass_of($files, self::PSR7_UPLOADED_FILE_CLASS)) {
return [self::extractFromUploadedFileInterface($files)];
}
if (get_class($files) == self::SYMFONY_UPLOADED_FILE_CLASS) {
return [self::extractFromSymfonyFile($files)];
}
}
// If caller passed in an array of objects (Either PSR7 or Symfony)
if (is_array($files) && is_object(reset($files))) {
$firstFile = reset($files);
if ($firstFile instanceof UploadedFileInterface) {
$result = [];
foreach ($files as $file) {
$result[] = self::extractFromUploadedFileInterface($file);
}
return $result;
}
if ($firstFile instanceof UploadedFile) {
$result = [];
foreach ($files as $file) {
$result[] = self::extractFromSymfonyFile($file);
}
return $result;
}
}
// The caller passed $_FILES['some_field_name']
if (isset($files['name'])) {
// we have a single file
if ( ! is_array($files['name'])) {
return [$files];
} else {
// we have list of files, which PHP messes up
return Helper::remapFilesArray($files); // @phpstan-ignore-line
}
} else {
// The caller passed $_FILES
$keys = array_keys($files); // @phpstan-ignore-line
if (isset($keys[0]) && isset($files[$keys[0]]['name'])) {
if ( ! is_array($files[$keys[0]]['name'])) {
// $files is in the correct format already, even in the
// case it contains a single element.
return $files; //@phpstan-ignore-line
} else {
// we have list of files, which PHP messes up
return Helper::remapFilesArray($files[$keys[0]]); // @phpstan-ignore-line
}
}
}
// If we got here, the $file argument is wrong
return [];
}
}
================================================
FILE: tests/.phpunit.result.cache
================================================
C:37:"PHPUnit\Runner\DefaultTestResultCache":2714:{a:2:{s:7:"defects";a:16:{s:64:"Sirius\Upload\HandlerTest::testExceptionTrwonForInvalidContainer";i:4;s:76:"Sirius\Upload\HandlerTest::testExceptionThrownForInvalidSanitizationCallback";i:4;s:47:"Sirius\Upload\HandlerAggregateTest::testProcess";i:4;s:48:"Sirius\Upload\HandlerTest::testPsr7UploadedFiles";i:3;s:51:"Sirius\Upload\HandlerTest::testSymfonyUploadedFiles";i:4;s:52:"Sirius\Upload\HandlerTest::testBasicUploadWithPrefix";i:4;s:46:"Sirius\Upload\HandlerTest::testUploadOverwrite";i:4;s:48:"Sirius\Upload\HandlerTest::testUploadAutoconfirm";i:4;s:55:"Sirius\Upload\HandlerTest::testSingleUploadConfirmation";i:4;s:51:"Sirius\Upload\HandlerTest::testSingleUploadClearing";i:4;s:42:"Sirius\Upload\HandlerTest::testMultiUpload";i:4;s:50:"Sirius\Upload\HandlerTest::testOriginalMultiUpload";i:4;s:46:"Sirius\Upload\HandlerTest::testWrongFilesArray";i:4;s:53:"Sirius\Upload\HandlerTest::testSingleUploadValidation";i:4;s:52:"Sirius\Upload\HandlerTest::testMultiUploadValidation";i:4;s:57:"Sirius\Upload\HandlerTest::testCustomSanitizationCallback";i:4;}s:5:"times";a:24:{s:43:"Sirius\Upload\Container\LocalTest::testSave";d:0.03;s:45:"Sirius\Upload\Container\LocalTest::testDelete";d:0.011;s:59:"Sirius\Upload\Container\LocalTest::testDeleteInexistingFile";d:0.007;s:55:"Sirius\Upload\Container\LocalTest::testMoveUploadedFile";d:0.014;s:62:"Sirius\Upload\Container\LocalTest::testMoveMissingUploadedFile";d:0.007;s:47:"Sirius\Upload\HandlerAggregateTest::testProcess";d:0.1;s:48:"Sirius\Upload\HandlerAggregateTest::testIterator";d:0.024;s:52:"Sirius\Upload\HandlerTest::testBasicUploadWithPrefix";d:0.019;s:46:"Sirius\Upload\HandlerTest::testUploadOverwrite";d:0.042;s:48:"Sirius\Upload\HandlerTest::testUploadAutoconfirm";d:0.018;s:55:"Sirius\Upload\HandlerTest::testSingleUploadConfirmation";d:0.024;s:51:"Sirius\Upload\HandlerTest::testSingleUploadClearing";d:0.024;s:42:"Sirius\Upload\HandlerTest::testMultiUpload";d:0.038;s:50:"Sirius\Upload\HandlerTest::testOriginalMultiUpload";d:0.032;s:46:"Sirius\Upload\HandlerTest::testWrongFilesArray";d:0.012;s:64:"Sirius\Upload\HandlerTest::testExceptionTrwonForInvalidContainer";d:0.013;s:53:"Sirius\Upload\HandlerTest::testSingleUploadValidation";d:0.017;s:52:"Sirius\Upload\HandlerTest::testMultiUploadValidation";d:0.018;s:57:"Sirius\Upload\HandlerTest::testCustomSanitizationCallback";d:0.022;s:76:"Sirius\Upload\HandlerTest::testExceptionThrownForInvalidSanitizationCallback";d:0.012;s:48:"Sirius\Upload\HandlerTest::testPsr7UploadedFiles";d:0.045;s:51:"Sirius\Upload\HandlerTest::testSymfonyUploadedFiles";d:0.07;s:56:"Sirius\Upload\HandlerTest::testSingleSymfonyUploadedFile";d:0.03;s:53:"Sirius\Upload\HandlerTest::testSinglePsr7UploadedFile";d:0.026;}}}
================================================
FILE: tests/Pest.php
================================================
<?php
/*
|--------------------------------------------------------------------------
| Test Case
|--------------------------------------------------------------------------
|
| The closure you provide to your test functions is always bound to a specific PHPUnit test
| case class. By default, that class is "PHPUnit\Framework\TestCase". Of course, you may
| need to change it using the "uses()" function to bind a different classes or traits.
|
*/
require_once __DIR__ . '/TestCase.php';
uses(Tests\TestCase::class)->in('src');
/*
|--------------------------------------------------------------------------
| Expectations
|--------------------------------------------------------------------------
|
| When you're writing tests, you often need to check that values meet certain conditions. The
| "expect()" function gives you access to a set of "expectations" methods that you can use
| to assert different things. Of course, you may extend the Expectation API at any time.
|
*/
expect()->extend('toBeOne', function () {
return $this->toBe(1);
});
/*
|--------------------------------------------------------------------------
| Functions
|--------------------------------------------------------------------------
|
| While Pest is very powerful out-of-the-box, you may have some testing code specific to your
| project that you don't want to repeat in every file. Here you can also expose helpers as
| global functions to help you to reduce the number of lines of code in your test files.
|
*/
function something()
{
// ..
}
================================================
FILE: tests/TestCase.php
================================================
<?php
namespace Tests;
use PHPUnit\Framework\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase
{
function createTemporaryFile($name, $content = "")
{
file_put_contents($this->tmpFolder . '/' . $name, $content);
}
}
================================================
FILE: tests/phpunit.xml
================================================
<phpunit bootstrap="phpunit_bootstrap.php"
backupGlobals="false"
backupStaticAttributes="false"
colors="true"
verbose="false"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
stopOnError="false">
<logging>
<log type="coverage-clover" target="../build/logs/clover.xml"/>
<log type="coverage-html" target="../build/coverage/"/>
</logging>
<filter>
<whitelist addUncoveredFilesFromWhitelist="true">
<directory suffix=".php">./../src/</directory>
</whitelist>
</filter>
<testsuites>
<testsuite name="Sirius Upload Test Suite">
<directory>./src/</directory>
</testsuite>
</testsuites>
</phpunit>
================================================
FILE: tests/phpunit_bootstrap.php
================================================
<?php
require_once(__DIR__ . '/../vendor/autoload.php');
error_reporting(E_ERROR);
================================================
FILE: tests/src/Container/LocalTest.php
================================================
<?php
use Sirius\Upload\Container\Local as LocalContainer;
function rrmdir($dir)
{
if (is_dir($dir)) {
$objects = scandir($dir);
foreach ($objects as $object) {
if ($object != "." && $object != "..") {
if (filetype($dir . "/" . $object) == "dir") {
rrmdir($dir . "/" . $object);
} else {
unlink($dir . "/" . $object);
}
}
}
reset($objects);
rmdir($dir);
}
}
beforeEach(function () {
$this->dir = realpath(__DIR__ . '/../../') . '/fixture/';
$this->container = new LocalContainer($this->dir);
});
afterEach(function () {
rrmdir($this->dir);
});
test('save', function () {
$file = 'subdir/test.txt';
expect($this->container->save($file, 'cool'))->toBeTrue();
expect(file_exists($this->dir . $file))->toBeTrue();
expect($this->container->has($file))->toBeTrue();
expect(file_get_contents($this->dir . $file))->toEqual('cool');
});
test('delete', function () {
$file = 'subdir/test.txt';
$this->container->save($file, 'cool');
expect(file_exists($this->dir . $file))->toBeTrue();
expect($this->container->delete($file))->toBeTrue();
expect(file_exists($this->dir . $file))->toBeFalse();
});
test('delete inexisting file', function () {
$file = 'subdir/test.txt';
expect($this->container->delete($file))->toBeTrue();
});
test('move uploaded file', function () {
$file = 'test.txt';
$file2 = 'sub/test.txt';
$this->container->save($file, 'cool');
expect($this->container->moveUploadedFile($this->dir . $file, $file2))->toBeTrue();
expect(file_get_contents($this->dir . $file2))->toEqual('cool');
});
test('move missing uploaded file', function () {
$file = 'subdir/test.txt';
expect($this->container->moveUploadedFile($this->dir . $file, $file))->toBeFalse();
});
================================================
FILE: tests/src/HandlerAggregateTest.php
================================================
<?php
use \Sirius\Upload\Handler;
use \Sirius\Upload\HandlerAggregate;
beforeEach(function () {
$this->tmpFolder = realpath(__DIR__ . '/../fixitures/');
if (!is_dir($this->tmpFolder)) {
@mkdir($this->tmpFolder . '/container');
}
$this->uploadFolder = realpath(__DIR__ . '/../fixitures/container/');
$this->agg = new HandlerAggregate();
$this->agg->addHandler(
'user_picture',
new Handler(
$this->uploadFolder . '/user_picture', array(
Handler::OPTION_PREFIX => '',
Handler::OPTION_OVERWRITE => false,
Handler::OPTION_AUTOCONFIRM => false
)
)
);
$this->agg->addHandler(
'resume',
new Handler(
$this->uploadFolder . '/resume', array(
Handler::OPTION_PREFIX => '',
Handler::OPTION_OVERWRITE => false,
Handler::OPTION_AUTOCONFIRM => false
)
)
);
$this->agg->addHandler(
'portfolio[photos]',
new Handler(
$this->uploadFolder . '/photo', array(
Handler::OPTION_PREFIX => '',
Handler::OPTION_OVERWRITE => false,
Handler::OPTION_AUTOCONFIRM => false
)
)
);
});
afterEach(function () {
$files = glob($this->uploadFolder . '/*');
// get all file names
foreach ($files as $file) { // iterate files
if (is_file($file)) {
unlink($file);
} // delete file
}
});
test('process', function () {
$this->createTemporaryFile('abc.tmp');
$this->createTemporaryFile('def.tmp');
$files = array(
'user_picture' => array(
'name' => 'pic.jpg',
'tmp_name' => $this->tmpFolder . '/abc.tmp'
),
'resume' => array(
'name' => 'resume.doc',
'tmp_name' => $this->tmpFolder . '/def.tmp'
)
);
$result = $this->agg->process($files);
expect(file_exists($this->uploadFolder . '/user_picture/' . $result['user_picture']->name))->toBeTrue();
expect(file_exists($this->uploadFolder . '/user_picture/' . $result['user_picture']->name . '.lock'))->toBeTrue();
$result->confirm();
expect(file_exists($this->uploadFolder . '/user_picture/' . $result['user_picture']->name . '.lock'))->toBeFalse();
});
test('iterator', function () {
$handlers = $this->agg->getIterator();
expect($handlers['user_picture'] instanceof Handler)->toBeTrue();
});
================================================
FILE: tests/src/HandlerTest.php
================================================
<?php
use Laminas\Diactoros\StreamFactory;
use \Sirius\Upload\Handler;
use Laminas\Diactoros\UploadedFile;
use Symfony\Component\HttpFoundation\File\UploadedFile as SymfonyUploadedFile;
beforeEach(function () {
$this->tmpFolder = realpath(__DIR__ . '/../fixitures/');
if (!is_dir($this->tmpFolder)) {
@mkdir($this->tmpFolder . '/container');
}
$this->uploadFolder = realpath(__DIR__ . '/../fixitures/container/');
$this->handler = new Handler(
$this->uploadFolder, array(
Handler::OPTION_PREFIX => '',
Handler::OPTION_OVERWRITE => false,
Handler::OPTION_AUTOCONFIRM => false
)
);
});
afterEach(function () {
$files = glob($this->uploadFolder . '/*');
// get all file names
foreach ($files as $file) { // iterate files
if (is_file($file)) {
unlink($file);
} // delete file
}
});
test('basic upload with prefix', function () {
$this->handler->setPrefix('subfolder/');
$this->createTemporaryFile('abc.tmp');
$result = $this->handler->process(
array(
'name' => 'abc.jpg',
'tmp_name' => $this->tmpFolder . '/abc.tmp'
)
);
expect(file_exists($this->uploadFolder . '/' . $result->name))->toBeTrue();
expect(file_exists($this->uploadFolder . '/' . $result->name . '.lock'))->toBeTrue();
// tearDown does not clean the subfolders
unlink($this->uploadFolder . '/' . $result->name);
unlink($this->uploadFolder . '/' . $result->name . '.lock');
});
test('upload overwrite', function () {
$this->createTemporaryFile('abc.tmp', 'first_file');
$result = $this->handler->process(
array(
'name' => 'abc.jpg',
'tmp_name' => $this->tmpFolder . '/abc.tmp'
)
);
expect('first_file')->toEqual(file_get_contents($this->uploadFolder . '/abc.jpg'));
// no overwrite, the first upload should be preserved
$this->handler->setOverwrite(false);
$this->createTemporaryFile('abc.tmp', 'second_file');
$result = $this->handler->process(
array(
'name' => 'abc.jpg',
'tmp_name' => $this->tmpFolder . '/abc.tmp'
)
);
expect('first_file')->toEqual(file_get_contents($this->uploadFolder . '/abc.jpg'));
// overwrite, the first uploaded file should be changed
$this->handler->setOverwrite(true);
$this->createTemporaryFile('abc.tmp', 'second_file');
$result = $this->handler->process(
array(
'name' => 'abc.jpg',
'tmp_name' => $this->tmpFolder . '/abc.tmp'
)
);
expect('second_file')->toEqual(file_get_contents($this->uploadFolder . '/abc.jpg'));
});
test('upload autoconfirm', function () {
$this->handler->setAutoconfirm(true);
$this->createTemporaryFile('abc.tmp', 'first_file');
$result = $this->handler->process(
array(
'name' => 'abc.jpg',
'tmp_name' => $this->tmpFolder . '/abc.tmp'
)
);
expect(file_exists($this->uploadFolder . '/' . $result->name))->toBeTrue();
expect(file_exists($this->uploadFolder . '/' . $result->name . '.lock'))->toBeFalse();
});
test('single upload confirmation', function () {
$this->createTemporaryFile('abc.tmp', 'first_file');
$result = $this->handler->process(
array(
'name' => 'abc.jpg',
'tmp_name' => $this->tmpFolder . '/abc.tmp'
)
);
expect(file_exists($this->uploadFolder . '/' . $result->name))->toBeTrue();
expect(file_exists($this->uploadFolder . '/' . $result->name . '.lock'))->toBeTrue();
$result->confirm();
expect(file_exists($this->uploadFolder . '/' . $result->name . '.lock'))->toBeFalse();
});
test('single upload clearing', function () {
$this->createTemporaryFile('abc.tmp', 'first_file');
$result = $this->handler->process(
array(
'name' => 'abc.jpg',
'tmp_name' => $this->tmpFolder . '/abc.tmp'
)
);
expect(file_exists($this->uploadFolder . '/' . $result->name))->toBeTrue();
expect(file_exists($this->uploadFolder . '/' . $result->name . '.lock'))->toBeTrue();
$fileName = $result->name;
$result->clear();
expect(file_exists($this->uploadFolder . '/' . $fileName))->toBeFalse();
expect(file_exists($this->uploadFolder . '/' . $fileName . '.lock'))->toBeFalse();
});
test('multi upload', function () {
$this->createTemporaryFile('abc.tmp', 'first_file');
$this->createTemporaryFile('def.tmp', 'first_file');
// array is already properly formatted
$result = $this->handler->process(
array(
array(
'name' => 'abc.jpg',
'tmp_name' => $this->tmpFolder . '/abc.tmp'
),
array(
'name' => 'def.jpg',
'tmp_name' => $this->tmpFolder . '/def.tmp'
)
)
);
expect($result->isValid())->toBeTrue();
# var_dump(glob($this->uploadFolder . '/*'));
foreach ($result as $file) {
expect(file_exists($this->uploadFolder . '/' . $file->name))->toBeTrue();
expect(file_exists($this->uploadFolder . '/' . $file->name . '.lock'))->toBeTrue();
}
// confirmation removes the .lock files
$result->confirm();
foreach ($result as $file) {
expect(file_exists($this->uploadFolder . '/' . $file->name))->toBeTrue();
expect(file_exists($this->uploadFolder . '/' . $file->name . '.lock'))->toBeFalse();
}
// clearing removes the uploaded files and their locks (which are already removed)
$result->clear();
foreach ($result as $file) {
expect($file->name)->toBeNull();
}
});
test('original multi upload', function () {
$this->createTemporaryFile('abc.tmp', 'first_file');
$this->createTemporaryFile('def.tmp', 'first_file');
// array is as provided by PHP
$result = $this->handler->process(
array(
'name' => array(
'abc.jpg',
'def.jpg',
),
'tmp_name' => array(
$this->tmpFolder . '/abc.tmp',
$this->tmpFolder . '/def.tmp'
),
)
);
expect(2)->toEqual(count($result));
foreach ($result as $file) {
expect(file_exists($this->uploadFolder . '/' . $file->name))->toBeTrue();
expect(file_exists($this->uploadFolder . '/' . $file->name . '.lock'))->toBeTrue();
}
});
test('wrong files array', function () {
$result = $this->handler->process(array('names' => 'abc.jpg'));
expect(0)->toEqual(count($result));
});
test('exception trwon for invalid container', function () {
$this->expectException('Sirius\Upload\Exception\InvalidContainerException');
$handler = new Handler(new \stdClass());
});
test('single upload validation', function () {
$this->createTemporaryFile('abc.tmp', 'non image file');
// uploaded files must be an image
$this->handler->addRule(Handler::RULE_IMAGE);
$result = $this->handler->process(
array(
'name' => 'abc.jpg',
'tmp_name' => $this->tmpFolder . '/abc.tmp'
)
);
expect($result->isValid())->toBeFalse();
expect(1)->toEqual(count($result->getMessages()));
expect($result->nonAttribute)->toBeNull();
});
test('multi upload validation', function () {
$this->createTemporaryFile('abc.tmp', 'first_file');
$this->createTemporaryFile('def.tmp', 'second_file');
// uploaded file must be an image
$this->handler->addRule(Handler::RULE_IMAGE);
// array is as provided by PHP
$result = $this->handler->process(
array(
'name' => array(
'abc.jpg',
'def.jpg',
),
'tmp_name' => array(
$this->tmpFolder . '/abc.tmp',
$this->tmpFolder . '/def.tmp'
),
)
);
$messages = $result->getMessages();
expect($result->isValid())->toBeFalse();
expect(2)->toEqual(count($messages));
expect(1)->toEqual(count($messages[0]));
});
test('custom sanitization callback', function () {
$this->handler->setSanitizerCallback(function ($name) {
return preg_replace('/[^A-Za-z0-9\.]+/', '-', strtolower($name));
});
$this->createTemporaryFile('ABC 123.tmp', 'non image file');
$result = $this->handler->process(
array(
'name' => 'ABC 123.tmp',
'tmp_name' => $this->tmpFolder . '/ABC 123.tmp'
)
);
expect(file_exists($this->uploadFolder . '/abc-123.tmp'))->toBeTrue();
});
test('psr7 uploaded files', function () {
$files = ['abc.tmp', 'def.tmp'];
$psr7Files = [];
foreach ($files as $file) {
$this->createTemporaryFile($file, 'first_file');
$factory = new StreamFactory();
$stream = $factory->createStreamFromFile($this->tmpFolder . '/' . $file);
$psr7Files[] = new UploadedFile(
$stream,
$stream->getSize(),
UPLOAD_ERR_OK,
$file
);
}
$result = $this->handler->process($psr7Files);
foreach ($result as $item) {
expect(file_exists($this->uploadFolder . '/' . $item->name))->toBeTrue();
expect(file_exists($this->uploadFolder . '/' . $item->name . '.lock'))->toBeTrue();
$item->confirm();
expect(file_exists($this->uploadFolder . '/' . $item->name . '.lock'))->toBeFalse();
}
});
test('single psr7 uploaded file', function () {
$file = 'abc.tmp';
$this->createTemporaryFile($file, 'first_file');
$factory = new StreamFactory();
$stream = $factory->createStreamFromFile($this->tmpFolder . '/' . $file);
$psr7File = new UploadedFile(
$stream,
$stream->getSize(),
UPLOAD_ERR_OK,
$file
);
$result = $this->handler->process($psr7File);
expect(file_exists($this->uploadFolder . '/' . $result->name))->toBeTrue();
expect(file_exists($this->uploadFolder . '/' . $result->name . '.lock'))->toBeTrue();
$result->confirm();
expect(file_exists($this->uploadFolder . '/' . $result->name . '.lock'))->toBeFalse();
});
test('symfony uploaded files', function () {
$files = ['abc.tmp', 'def.tmp'];
$symfonyFiles = [];
foreach ($files as $file) {
$this->createTemporaryFile($file, 'first_file');
$symfonyFiles[] = new SymfonyUploadedFile($this->tmpFolder . '/' . $file, $file);
}
$result = $this->handler->process($symfonyFiles);
foreach ($result as $item) {
expect(file_exists($this->uploadFolder . '/' . $item->name))->toBeTrue();
expect(file_exists($this->uploadFolder . '/' . $item->name . '.lock'))->toBeTrue();
$item->confirm();
expect(file_exists($this->uploadFolder . '/' . $item->name . '.lock'))->toBeFalse();
}
});
test('single symfony uploaded file', function () {
$file = 'abc.tmp';
$this->createTemporaryFile($file, 'first_file');
$symfonyFile = new SymfonyUploadedFile($this->tmpFolder . '/' . $file, $file);
$result = $this->handler->process($symfonyFile);
expect(file_exists($this->uploadFolder . '/' . $result->name))->toBeTrue();
expect(file_exists($this->uploadFolder . '/' . $result->name . '.lock'))->toBeTrue();
$result->confirm();
expect(file_exists($this->uploadFolder . '/' . $result->name . '.lock'))->toBeFalse();
});
================================================
FILE: tests/web/index.php
================================================
<?php
include('../../autoload.php');
include('../../vendor/autoload.php');
$destination = realpath('../fixitures/container');
$pictureHandler = new Sirius\Upload\Handler($destination);
$pictureHandler->addRule(Sirius\Upload\Handler::RULE_IMAGE, array('allowed' => array('jpg', 'png')));
$resumeHandler = new Sirius\Upload\Handler($destination);
$resumeHandler->addRule(Sirius\Upload\Handler::RULE_EXTENSION, array('allowed' => array('doc', 'docx', 'pdf')));
$upload = new Sirius\Upload\HandlerAggregate();
$upload->addHandler('picture', $pictureHandler);
$upload->addHandler('resume', $resumeHandler);
$result = false;
if ($_FILES) {
$result = $upload->process($_FILES);
}
?>
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title>Sirius\Upload test</title>
</head>
<body>
<?php if ($result) { ?>
<?php foreach ($result as $key => $file) { ?>
<h3><?php echo $key ?> upload results:</h3>
<?php if ($file->isValid()) { ?>
SUCCESS! Uploaded as <?php echo $file->name ?>
<?php } else { ?>
ERROR! Error messages:<br/>
<?php echo implode('<br>', $file->getMessages());?>
<?php } ?>
<?php } ?>
<?php } else { ?>
<form method="post" enctype="multipart/form-data">
<div>Profile picture: <input type="file" name="picture"></div>
<div>Resume: <input type="file" name="resume"></div>
<input type="submit" value="Send">
</form>
<?php } ?>
</body>
</html>
gitextract_5xugku7m/
├── .gitattributes
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── Bug.md
│ │ ├── Feature_Request.md
│ │ └── Question.md
│ └── workflows/
│ ├── ci.yml
│ └── docs.yml
├── .gitignore
├── .php_cs.cache
├── .scrutinizer.yml
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── autoload.php
├── composer.json
├── docs/
│ ├── cloud_upload.md
│ ├── couscous.yml
│ ├── custom_validation.md
│ ├── file_locking.md
│ ├── index.md
│ ├── installation.md
│ ├── integrations.md
│ ├── simple_example.md
│ ├── upload_aggregator.md
│ ├── upload_options.md
│ └── validation_rules.md
├── phpstan.neon
├── phpunit.xml
├── src/
│ ├── Container/
│ │ ├── ContainerInterface.php
│ │ └── Local.php
│ ├── Exception/
│ │ ├── InvalidContainerException.php
│ │ └── InvalidResultException.php
│ ├── Handler.php
│ ├── HandlerAggregate.php
│ ├── Result/
│ │ ├── Collection.php
│ │ ├── File.php
│ │ └── ResultInterface.php
│ ├── UploadHandlerInterface.php
│ └── Util/
│ └── Helper.php
└── tests/
├── .phpunit.result.cache
├── Pest.php
├── TestCase.php
├── phpunit.xml
├── phpunit_bootstrap.php
├── src/
│ ├── Container/
│ │ └── LocalTest.php
│ ├── HandlerAggregateTest.php
│ └── HandlerTest.php
└── web/
└── index.php
SYMBOL INDEX (61 symbols across 14 files)
FILE: src/Container/ContainerInterface.php
type ContainerInterface (line 6) | interface ContainerInterface
method isWritable (line 12) | public function isWritable(): bool;
method has (line 17) | public function has(string $file): bool;
method save (line 25) | public function save(string $file, string $content): bool;
method delete (line 30) | public function delete(string $file): bool;
method moveUploadedFile (line 35) | public function moveUploadedFile(string $localFile, string $destinatio...
FILE: src/Container/Local.php
class Local (line 6) | class Local implements ContainerInterface
method __construct (line 10) | public function __construct(string $baseDirectory)
method normalizePath (line 16) | protected function normalizePath(string $path): string
method ensureDirectory (line 23) | protected function ensureDirectory(string $directory): bool
method isWritable (line 35) | public function isWritable(): bool
method has (line 47) | public function has(string $file): bool
method save (line 52) | public function save(string $file, string $content): bool
method delete (line 63) | public function delete(string $file): bool
method moveUploadedFile (line 73) | public function moveUploadedFile(string $localFile, string $destinatio...
FILE: src/Exception/InvalidContainerException.php
class InvalidContainerException (line 6) | class InvalidContainerException extends \RuntimeException
FILE: src/Exception/InvalidResultException.php
class InvalidResultException (line 6) | class InvalidResultException extends \RuntimeException
FILE: src/Handler.php
class Handler (line 13) | class Handler implements UploadHandlerInterface
method __construct (line 63) | public function __construct(mixed $directoryOrContainer, array $option...
method setOverwrite (line 93) | public function setOverwrite(bool $overwrite): self
method setPrefix (line 108) | public function setPrefix(mixed $prefix): self
method setAutoconfirm (line 119) | public function setAutoconfirm(bool $autoconfirm): self
method setSanitizerCallback (line 130) | public function setSanitizerCallback(callable|\Closure $callback): self
method addRule (line 145) | public function addRule(string $name, array $options = [], string $err...
method process (line 170) | public function process(mixed $files): ResultInterface
method processSingleFile (line 195) | protected function processSingleFile(array $file): array
method validateFile (line 249) | protected function validateFile(array $file): array
method sanitizeFileName (line 262) | protected function sanitizeFileName(string $name): string
FILE: src/HandlerAggregate.php
class HandlerAggregate (line 9) | class HandlerAggregate implements \IteratorAggregate
method addHandler (line 19) | public function addHandler(string $selector, Handler $handler): self
method process (line 31) | public function process(mixed $files): mixed
method getIterator (line 52) | public function getIterator(): \Traversable
FILE: src/Result/Collection.php
class Collection (line 8) | class Collection extends \ArrayIterator implements ResultInterface
method __construct (line 13) | public function __construct(array $files = [], ContainerInterface $con...
method clear (line 24) | public function clear(): void
method confirm (line 32) | public function confirm(): void
method isValid (line 40) | public function isValid(): bool
method getMessages (line 54) | public function getMessages(): array
FILE: src/Result/File.php
class File (line 11) | class File implements ResultInterface
method __construct (line 34) | public function __construct(array $file, ContainerInterface $container)
method isValid (line 45) | public function isValid(): bool
method getMessages (line 55) | public function getMessages(): array
method clear (line 68) | public function clear(): void
method confirm (line 79) | public function confirm(): void
method __get (line 84) | public function __get(string $name): mixed
FILE: src/Result/ResultInterface.php
type ResultInterface (line 6) | interface ResultInterface
method isValid (line 13) | public function isValid(): bool;
method getMessages (line 20) | public function getMessages(): array;
method clear (line 26) | public function clear(): void;
method confirm (line 32) | public function confirm(): void;
FILE: src/UploadHandlerInterface.php
type UploadHandlerInterface (line 10) | interface UploadHandlerInterface
method process (line 25) | public function process(mixed $files): mixed;
FILE: src/Util/Helper.php
class Helper (line 9) | class Helper
method extractFromUploadedFileInterface (line 18) | public static function extractFromUploadedFileInterface(UploadedFileIn...
method extractFromSymfonyFile (line 39) | public static function extractFromSymfonyFile(UploadedFile $file): array
method remapFilesArray (line 57) | public static function remapFilesArray(array $files): array
method normalizeFiles (line 84) | public static function normalizeFiles(mixed $files): array
FILE: tests/Pest.php
function something (line 42) | function something()
FILE: tests/TestCase.php
class TestCase (line 7) | abstract class TestCase extends BaseTestCase
method createTemporaryFile (line 9) | function createTemporaryFile($name, $content = "")
FILE: tests/src/Container/LocalTest.php
function rrmdir (line 6) | function rrmdir($dir)
Condensed preview — 48 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (82K chars).
[
{
"path": ".gitattributes",
"chars": 272,
"preview": "/docs export-ignore\n/tests export-ignore\n/.gitattributes export-ignore\n/.gitignore "
},
{
"path": ".github/ISSUE_TEMPLATE/Bug.md",
"chars": 429,
"preview": "---\nname: 🐛 Bug\nabout: Did you encounter a bug?\n---\n\n### Bug Report\n\n<!-- Fill in the relevant information below to help"
},
{
"path": ".github/ISSUE_TEMPLATE/Feature_Request.md",
"chars": 457,
"preview": "---\nname: 🎉 Feature Request\nabout: Do you have a new feature in mind?\n---\n\n### Feature Request\n\n<!-- Fill in the relevan"
},
{
"path": ".github/ISSUE_TEMPLATE/Question.md",
"chars": 154,
"preview": "---\nname: ❓ Question\nabout: Are you unsure about something?\n---\n\n### Question\n\n<!-- Fill in the relevant information bel"
},
{
"path": ".github/workflows/ci.yml",
"chars": 1174,
"preview": "name: CI\n\non:\n push:\n branches: [ master ]\n pull_request:\n branches: [ master ]\n\njobs:\n build:\n strategy:\n "
},
{
"path": ".github/workflows/docs.yml",
"chars": 733,
"preview": "name: Publish docs\n\non:\n push:\n tags:\n - '*.*.*'\njobs:\n docs:\n runs-on: ubuntu-latest\n steps:\n - us"
},
{
"path": ".gitignore",
"chars": 182,
"preview": ".idea/\n.settings/\n.buildpath\n.project\nbuild/\nvendor/\ncomposer.lock\natlassian-ide-plugin.xml\ndocs/couscous.phar\ndocs/.cou"
},
{
"path": ".php_cs.cache",
"chars": 1346,
"preview": "{\"php\":\"7.2.4\",\"version\":\"2.16.1\",\"indent\":\" \",\"lineEnding\":\"\\n\",\"rules\":{\"blank_line_after_namespace\":true,\"braces\":"
},
{
"path": ".scrutinizer.yml",
"chars": 206,
"preview": "filter:\n paths: [\"src/*\"]\ntools:\n external_code_coverage: true\n php_code_coverage: true\n php_sim: true\n p"
},
{
"path": ".travis.yml",
"chars": 916,
"preview": "sudo: false\nlanguage: php\n\nmatrix:\n include:\n - php: 7.1\n dist: bionic\n env: DEPENDENCIES='low'\n - php:"
},
{
"path": "CHANGELOG.md",
"chars": 237,
"preview": "# CHANGELOG\n\n## 2.0.0\n\n- changed the `__constructor` parameters. Now you inject an optional `Sirius\\Validation\\ValueVali"
},
{
"path": "LICENSE",
"chars": 1077,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2014 Adrian Miu\n\nPermission is hereby granted, free of charge, to any person obtain"
},
{
"path": "README.md",
"chars": 2798,
"preview": "# Sirius\\Upload\n\n[](https"
},
{
"path": "autoload.php",
"chars": 937,
"preview": "<?php\nspl_autoload_register(function ($class) {\n \n // what namespace prefix should be recognized?\n $prefix = 'S"
},
{
"path": "composer.json",
"chars": 1632,
"preview": "{\n \"name\": \"siriusphp/upload\",\n \"description\": \"Framework agnostic upload library\",\n \"type\": \"library\",\n \"li"
},
{
"path": "docs/cloud_upload.md",
"chars": 608,
"preview": "---\ntitle: Upload into the cloud | Sirius Upload\n---\n\n# Upload into the cloud\n\nIf you want to store uploaded files in di"
},
{
"path": "docs/couscous.yml",
"chars": 2400,
"preview": "template:\n url: https://github.com/siriusphp/Template-ReadTheDocs\n\n# List of directories to exclude from the processi"
},
{
"path": "docs/custom_validation.md",
"chars": 1186,
"preview": "---\ntitle: Custom upload validation | Sirius Upload\n---\n\n# Custom validation\n\nIf you need implement specific file upload"
},
{
"path": "docs/file_locking.md",
"chars": 1123,
"preview": "---\ntitle: File locking during uploads | Sirius Upload\n---\n\n# What is 'file locking'?\n\nUsually, an application accepts f"
},
{
"path": "docs/index.md",
"chars": 4082,
"preview": "[](https://github.com/sir"
},
{
"path": "docs/installation.md",
"chars": 606,
"preview": "---\ntitle: Installing Sirius\\Upload | Sirius Upload\n---\n\n#Installation\n\n## Using composer\n\nSirius\\Validation is availabl"
},
{
"path": "docs/integrations.md",
"chars": 841,
"preview": "---\ntitle: Integrations | Sirius Upload\n---\n\n# Integrations with other libraries\n\n## PSR-7's UploadedFileInterface\n\nIf y"
},
{
"path": "docs/simple_example.md",
"chars": 1389,
"preview": "---\ntitle: Simple upload example | Sirius Upload\n---\n\n# Simple example\n\n### Initialize the upload handler class\n\nLet's c"
},
{
"path": "docs/upload_aggregator.md",
"chars": 1301,
"preview": "---\ntitle: The upload aggregator | Sirius Upload\n---\n\n# The upload aggregator\n\nSometimes your form may upload multiple f"
},
{
"path": "docs/upload_options.md",
"chars": 1817,
"preview": "---\ntitle: Upload options | Sirius Upload\n---\n\n#Upload options\n\nThere are a few options you can choose to use while usin"
},
{
"path": "docs/validation_rules.md",
"chars": 1628,
"preview": "---\ntitle: File upload validation rules | Sirius Upload\n---\n\n# Upload validation rules\n\nFor validating the uploads the l"
},
{
"path": "phpstan.neon",
"chars": 86,
"preview": "parameters:\n\tlevel: 8\n\tcheckGenericClassInNonGenericObjectType: false\n\tpaths:\n\t\t- src\n"
},
{
"path": "phpunit.xml",
"chars": 592,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n xsi:noNam"
},
{
"path": "src/Container/ContainerInterface.php",
"chars": 783,
"preview": "<?php\ndeclare(strict_types=1);\n\nnamespace Sirius\\Upload\\Container;\n\ninterface ContainerInterface\n{\n\n /**\n * Check"
},
{
"path": "src/Container/Local.php",
"chars": 2363,
"preview": "<?php\ndeclare(strict_types=1);\n\nnamespace Sirius\\Upload\\Container;\n\nclass Local implements ContainerInterface\n{\n prot"
},
{
"path": "src/Exception/InvalidContainerException.php",
"chars": 130,
"preview": "<?php\ndeclare(strict_types=1);\n\nnamespace Sirius\\Upload\\Exception;\n\nclass InvalidContainerException extends \\RuntimeExce"
},
{
"path": "src/Exception/InvalidResultException.php",
"chars": 127,
"preview": "<?php\ndeclare(strict_types=1);\n\nnamespace Sirius\\Upload\\Exception;\n\nclass InvalidResultException extends \\RuntimeExcepti"
},
{
"path": "src/Handler.php",
"chars": 8149,
"preview": "<?php\ndeclare(strict_types=1);\n\nnamespace Sirius\\Upload;\n\nuse Sirius\\Upload\\Container\\ContainerInterface;\nuse Sirius\\Upl"
},
{
"path": "src/HandlerAggregate.php",
"chars": 1280,
"preview": "<?php\ndeclare(strict_types=1);\n\nnamespace Sirius\\Upload;\n\nuse Sirius\\Upload\\Result\\Collection;\nuse Sirius\\Validation\\Uti"
},
{
"path": "src/Result/Collection.php",
"chars": 1488,
"preview": "<?php\ndeclare(strict_types=1);\n\nnamespace Sirius\\Upload\\Result;\n\nuse Sirius\\Upload\\Container\\ContainerInterface;\n\nclass "
},
{
"path": "src/Result/File.php",
"chars": 2079,
"preview": "<?php\ndeclare(strict_types=1);\n\nnamespace Sirius\\Upload\\Result;\n\nuse Sirius\\Upload\\Container\\ContainerInterface;\n\n/**\n *"
},
{
"path": "src/Result/ResultInterface.php",
"chars": 757,
"preview": "<?php\ndeclare(strict_types=1);\n\nnamespace Sirius\\Upload\\Result;\n\ninterface ResultInterface\n{\n /**\n * Returns if t"
},
{
"path": "src/UploadHandlerInterface.php",
"chars": 786,
"preview": "<?php\ndeclare(strict_types=1);\n\nnamespace Sirius\\Upload;\n\nuse Psr\\Http\\Message\\UploadedFileInterface;\nuse Sirius\\Upload\\"
},
{
"path": "src/Util/Helper.php",
"chars": 4973,
"preview": "<?php\ndeclare(strict_types=1);\n\nnamespace Sirius\\Upload\\Util;\n\nuse Psr\\Http\\Message\\UploadedFileInterface;\nuse Symfony\\C"
},
{
"path": "tests/.phpunit.result.cache",
"chars": 2766,
"preview": "C:37:\"PHPUnit\\Runner\\DefaultTestResultCache\":2714:{a:2:{s:7:\"defects\";a:16:{s:64:\"Sirius\\Upload\\HandlerTest::testExcepti"
},
{
"path": "tests/Pest.php",
"chars": 1541,
"preview": "<?php\n\n/*\n|--------------------------------------------------------------------------\n| Test Case\n|---------------------"
},
{
"path": "tests/TestCase.php",
"chars": 259,
"preview": "<?php\n\nnamespace Tests;\n\nuse PHPUnit\\Framework\\TestCase as BaseTestCase;\n\nabstract class TestCase extends BaseTestCase\n{"
},
{
"path": "tests/phpunit.xml",
"chars": 874,
"preview": "<phpunit bootstrap=\"phpunit_bootstrap.php\"\n backupGlobals=\"false\"\n backupStaticAttributes=\"false\"\n "
},
{
"path": "tests/phpunit_bootstrap.php",
"chars": 85,
"preview": "<?php\n\nrequire_once(__DIR__ . '/../vendor/autoload.php');\n\nerror_reporting(E_ERROR);\n"
},
{
"path": "tests/src/Container/LocalTest.php",
"chars": 1916,
"preview": "<?php\n\nuse Sirius\\Upload\\Container\\Local as LocalContainer;\n\n\nfunction rrmdir($dir)\n{\n if (is_dir($dir)) {\n $o"
},
{
"path": "tests/src/HandlerAggregateTest.php",
"chars": 2511,
"preview": "<?php\n\nuse \\Sirius\\Upload\\Handler;\nuse \\Sirius\\Upload\\HandlerAggregate;\n\nbeforeEach(function () {\n $this->tmpFolder ="
},
{
"path": "tests/src/HandlerTest.php",
"chars": 11527,
"preview": "<?php\n\nuse Laminas\\Diactoros\\StreamFactory;\nuse \\Sirius\\Upload\\Handler;\nuse Laminas\\Diactoros\\UploadedFile;\nuse Symfony\\"
},
{
"path": "tests/web/index.php",
"chars": 1472,
"preview": "<?php\ninclude('../../autoload.php');\ninclude('../../vendor/autoload.php');\n\n$destination = realpath('../fixitures/contai"
}
]
About this extraction
This page contains the full source code of the siriusphp/upload GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 48 files (74.3 KB), approximately 19.8k tokens, and a symbol index with 61 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.