Full Code of siriusphp/upload for AI

master 219a10ec6806 cached
48 files
74.3 KB
19.8k tokens
61 symbols
1 requests
Download .txt
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

[![Source Code](https://img.shields.io/badge/source-siriusphp/upload-blue.svg?style=flat-square)](https://github.com/siriusphp/upload)
[![Latest Version](https://img.shields.io/packagist/v/siriusphp/upload.svg?style=flat-square)](https://github.com/siriusphp/upload/releases)
[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](https://github.com/siriusphp/upload/blob/master/LICENSE)
[![Build Status](https://github.com/siriusphp/upload/actions/workflows/ci.yml/badge.svg)](https://github.com/siriusphp/upload/actions/workflows/ci.yml)
[![Coverage Status](https://img.shields.io/scrutinizer/coverage/g/siriusphp/upload.svg?style=flat-square)](https://scrutinizer-ci.com/g/siriusphp/upload/code-structure)
[![Quality Score](https://img.shields.io/scrutinizer/g/siriusphp/upload.svg?style=flat-square)](https://scrutinizer-ci.com/g/siriusphp/upload)
[![Total Downloads](https://img.shields.io/packagist/dt/siriusphp/upload.svg?style=flat-square)](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
================================================
[![Source Code](https://img.shields.io/badge/source-siriusphp/upload-blue.svg?style=flat-square)](https://github.com/siriusphp/upload)
[![Latest Version](https://img.shields.io/packagist/v/siriusphp/upload.svg?style=flat-square)](https://github.com/siriusphp/upload/releases)
[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](https://github.com/siriusphp/upload/blob/master/LICENSE)
[![Build Status](https://img.shields.io/travis/siriusphp/upload/master.svg?style=flat-square)](https://travis-ci.org/siriusphp/upload)
[![PHP 7 ready](https:////php7ready.timesplinter.ch/siriusphp/upload/master/badge.svg)](https://travis-ci.org/siriusphp/upload)
[![Coverage Status](https://img.shields.io/scrutinizer/coverage/g/siriusphp/upload.svg?style=flat-square)](https://scrutinizer-ci.com/g/siriusphp/upload/code-structure)
[![Quality Score](https://img.shields.io/scrutinizer/g/siriusphp/upload.svg?style=flat-square)](https://scrutinizer-ci.com/g/siriusphp/upload)
[![Total Downloads](https://img.shields.io/packagist/dt/siriusphp/upload.svg?style=flat-square)](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>
Download .txt
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
Download .txt
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[![Source Code](https://img.shields.io/badge/source-siriusphp/upload-blue.svg?style=flat-square)](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": "[![Source Code](https://img.shields.io/badge/source-siriusphp/upload-blue.svg?style=flat-square)](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.

Copied to clipboard!