diff --git a/README.md b/README.md
index 46d506e..fca046c 100644
--- a/README.md
+++ b/README.md
@@ -1,15 +1,22 @@
-# Image Storage
+
Image Storage
-:city_sunset: Extension for [68publishers/file-storage](https://github.com/68publishers/file-storage) that can generate images on-the-fly and more!
+:city_sunset: Extension for 68publishers/file-storage that can generate images on-the-fly and more!
+Based on thephpleague/flysystem and intervention/image
-Based on [thephpleague/flysystem](https://github.com/thephpleague/flysystem) and [intervention/image](https://github.com/Intervention/image).
+
+
+
+
+
+
+
## Installation
The best way to install 68publishers/image-storage is using Composer:
-```bash
-composer require 68publishers/image-storage
+```sh
+$ composer require 68publishers/image-storage
```
## Integration into Nette Framework
@@ -18,7 +25,7 @@ Firstly, please read a documentation of [68publishers/file-storage](https://gith
### File storage configuration example
-Each image-storage is based on file-storage. so firstly we need to register our storage under file-storage extension.
+Each image-storage is based on file-storage. so firstly we need to register our storage under the file-storage extension.
Here is an example configuration:
```neon
@@ -36,51 +43,54 @@ Here is an example configuration:
filesystem:
adapter: League\Flysystem\Local\LocalFilesystemAdapter(%wwwDir%/images)
assets:
- assets/image/noimage.png: noimage/default.png # copy our default no-image
- assets/image/noimage_user.png: noimage/user.png # copy our default no-image
+ assets/image/noimage.png: noimage/default.png # copy the default no-image
+ assets/image/noimage_user.png: noimage/user.png # copy the default no-image for users
```
#### Storage config options
-name | type | default | description
----- | ---- | ---- | ----
-base_path | string | `''` | Base path to a directory where the files are accessible.
-host | null or string | `null` | Hostname, use if the files are not stored locally or if you want to generate an absolute links
-version_parameter_name | string | `_v` | A query parameter's name used for a file's version (just for a cache).
-signature_parameter_name | string | `_s` | A query parameter's name used for a signature token.
-signature_key | null or string | `null` | Your private signature key used for a token encryption. Signatures in requests are checked and validated only if this parameter is set.
-signature_algorithm | string | `sha256` | An algorithm used for encryption of signatures (HMAC).
-modifier_separator | string | `,` | A separator for modifier definitions in a path. For example if you set this parameter as `;` then a modifier string in a path will look like this: `w:100;o:auto`.
-modifier_assigner | string | `:` | An assigner for modifier definitions in a path. For example if you set this parameter as `=` then a modifier string in a path will look like this: `w=100,o=auto`.
-allowed_pixel_density | int[] or float[] | `[]` | An array of allowed pixed densities. The validation is enabled when the array is not empty.
-allowed_resolutions | string[] | `[]` | An array of allowed resolutions like `100x`, `x200` or `100x200`. The validation is enabled when the array is not empty.
-allowed_qualities | int[] | `[]` | An array of allowed qualities. The validation is enabled when the array is not empty.
-encode_quality | int | `90` | An encode quality for cached images.
-cache_max_age | int | `31536000` | The maximum cache age in seconds. The value is used for HTTP headers Cache-Control and Expires.
+| Name | Type | Default | Description |
+|--------------------------|----------------------------|------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| base_path | string | `''` | Base path to a directory where the files are accessible. |
+| host | null or string | `null` | Hostname, use if the files are not stored locally or if you want to generate an absolute links |
+| version_parameter_name | string | `_v` | A query parameter's name used for a file's version (just for a cache). |
+| signature_parameter_name | string | `_s` | A query parameter's name used for a signature token. |
+| signature_key | null or string | `null` | Your private signature key used for a token encryption. Signatures in requests are checked and validated only if this parameter is set. |
+| signature_algorithm | string | `sha256` | An algorithm used for encryption of signatures (HMAC). |
+| modifier_separator | string | `,` | A separator for modifier definitions in a path. For example if you set this parameter as `;` then a modifier string in a path will look like this: `w:100;o:auto`. |
+| modifier_assigner | string | `:` | An assigner for modifier definitions in a path. For example if you set this parameter as `=` then a modifier string in a path will look like this: `w=100,o=auto`. |
+| allowed_pixel_density | array or array | `[]` | An array of allowed pixed densities. The validation is enabled when the array is not empty. |
+| allowed_resolutions | array | `[]` | An array of allowed resolutions like `100x`, `x200` or `100x200`. The validation is enabled when the array is not empty. |
+| allowed_qualities | array | `[]` | An array of allowed qualities. The validation is enabled when the array is not empty. |
+| encode_quality | int | `90` | An encode quality for cached images. |
+| cache_max_age | int | `31536000` | The maximum cache age in seconds. The value is used for HTTP headers Cache-Control and Expires. |
### Image storage configuration example
-Now we can register a `ImageStorageExtension` and define the `local` image-storage:
+Now we can register the `ImageStorageExtension` and define the `local` image-storage:
```neon
extensions:
68publishers.image_storage: SixtyEightPublishers\ImageStorage\Bridge\Nette\DI\ImageStorageExtension
68publishers.image_storage:
- driver: gd # "gd" or "imagick" or "68publishers.imagick", default is "gd"
+ driver: gd # "gd" or "imagick" or "68publishers.imagick", the default is "gd"
storages:
local:
source_filesystem:
adapter: League\Flysystem\Local\LocalFilesystemAdapter(%appDir%/../private-data/images)
- config: # an optional config for source filesystem adapter
- server: local # "local" or "external", default is "local"
+ config: [] # an optional config for source filesystem adapter
+ server: local # "local" or "external", the default is "local"
+ route: yes # registers automatically ImageServer presenter into your Router. The option can be applied only if the "server" option is set to "local" and the option "base_path" is set in the FileStorage config
no_image:
default: noimage/default.png
user: noimage/user.png
no_image_patterns:
user: '^user_avatar\/' # the noimage "user" will be used for missing files with paths that matches this regex
presets:
- my_preset: {w: 150, ar: '2x1.5'}
+ my_preset:
+ w: 150
+ ar: '2x1.5'
```
### Animated GIFs
@@ -97,9 +107,9 @@ Basic usage is similar to usage of the `file-storage`.
Files persisting is almost the same as persisting in the `file-storage` but source images are stored without a file extension.
```php
-createResourceFromLocalFile(
@@ -123,9 +133,9 @@ $storage->save($resource->withPathInfo(
#### Check a file existence
```php
-createPathInfo('test/my-image');
@@ -137,7 +147,7 @@ if ($storage->exists($pathInfo->withModifiers(['w' => 150]))) {
echo 'cached image with width 150 in JPEG (default) format exists!';
}
-if ($storage->exists($pathInfo->withModifiers(['w' => 150])->withExt('webp'))) {
+if ($storage->exists($pathInfo->withModifiers(['w' => 150])->withExtension('webp'))) {
echo 'cached image with width 150 in WEBP format exists!';
}
```
@@ -145,11 +155,10 @@ if ($storage->exists($pathInfo->withModifiers(['w' => 150])->withExt('webp'))) {
#### Deleting files
```php
-delete($storage->createPathInfo('test/my-image'), [
@@ -168,22 +177,22 @@ $storage->delete($storage->createPathInfo('test/my-image.png')->withModifiers(['
An original images are not accessible. If you want to access an original image you must request it with a modifier `['original' => TRUE]`.
```php
-createPathInfo('test/my-image.png')
->withModifiers(['original' => TRUE])
- ->setVersion(time());
+ ->withVersion(time());
# /images/test/original/my-image.png?_v=1611837352
echo $storage->link($pathInfo);
# /images/test/original/my-image.webp?_v=1611837352
-echo $storage->link($pathInfo->withExt('webp'));
+echo $storage->link($pathInfo->withExtension('webp'));
# /images/test/ar:2x1,w:200/my-image.webp?_v=1611837352&_s={GENERATED_SIGNATURE_TOKEN}
-echo $storage->link($pathInfo->withExt('webp')->withModifiers(['w' => 200, 'ar' => '2x1']));
+echo $storage->link($pathInfo->withExtension('webp')->withModifiers(['w' => 200, 'ar' => '2x1']));
# you can also wrap PathInfo to FileInfo object:
$fileInfo = $storage->createFileInfo($pathInfo);
@@ -192,25 +201,24 @@ $fileInfo = $storage->createFileInfo($pathInfo);
echo $fileInfo->link();
# /images/test/original/my-image.webp?_v=1611837352
-echo $fileInfo->withExt('webp')->link();
+echo $fileInfo->withExtension('webp')->link();
# /images/test/ar:2x1,w:200/my-image.webp?_v=1611837352&_s={GENERATED_SIGNATURE_TOKEN}
-echo $fileInfo->withExt('webp')->withModifiers(['w' => 200, 'ar' => '2x1'])->link();
+echo $fileInfo->withExtension('webp')->withModifiers(['w' => 200, 'ar' => '2x1'])->link();
```
The HTML attribute `srcset` can be also generated:
```php
-createPathInfo('test/my-image.png')
->withModifiers(['w' => 200, 'ar' => '2x1'])
- ->setVersion(time());
+ ->withVersion(time());
/*
/images/test/ar:2x1,pd:1,w:200/my-image.png?_v=1611837352&_s={TOKEN} ,
@@ -239,21 +247,14 @@ echo $fileInfo->srcSet(new WDescriptor(200, 400, 600, 800));
```neon
extensions:
68publishers.image_storage.latte: SixtyEightPublishers\ImageStorage\Bridge\Nette\DI\ImageStorageLatteExtension
-
-68publishers.image_storage.latte:
- function_names:
- create_w_descriptor: w_descriptor # default
- create_x_descriptor: x_descriptor # default
- create_w_descriptor_from_range: w_descriptor_range # default
- create_no_image: no_image # default
```
The extension adds these functions into the Latte:
- `w_descriptor(...)` - a shortcut for `new SixtyEightPublishers\ImageStorage\Responsive\Descriptor\XDescriptor(...)`
- `x_descriptor(...)` - a shortcut for `new SixtyEightPublishers\ImageStorage\Responsive\Descriptor\WDescriptor(...)`
-- `w_descriptor_range($min, $max, $step)` - a shortcut for `SixtyEightPublishers\ImageStorage\Responsive\Descriptor\WDescriptor::fromRange($min, $max, $step)`
-- `no_image(?string $noImageName = NULL, ?string $storageName = NULL)`
+- `w_descriptor_range(int $min, int $max, int $step)` - a shortcut for `SixtyEightPublishers\ImageStorage\Responsive\Descriptor\WDescriptor::fromRange($min, $max, $step)`
+- `no_image(?string $noImageName = NULL, ?string $storageName = NULL)` - creates a FilInfo object that contains path to no-image file
Basic usage:
@@ -311,16 +312,16 @@ $ bin/console file-storage:clean [] [--namespace ] [--cache-only
### Modifiers
-| Name | Shortcut | Type | Note |
-| --- | --- | --- | --- |
-| Original | original | - | A modifier without a value, use it if you want to return the original image |
-| Height | h | Integer | Can be restricted by parameter `AllowedResolutions` |
-| Width | w | Integer | Can be restricted by parameter `AllowedResolutions` |
-| Pixel density | pd | Integer\|Float | Can be restricted by parameter `AllowedPixelDensity` |
-| Aspect ratio | ar | String | Required format is `{Int\|Float}x{Int\|Float}` and a height or a width (not both) must be also defined. For example `w:200,ar:1x2` is an equivalent of `w:200,h:400` |
-| Fit | f | String | See [supported fits](#supported-fits) for the list of supported values |
-| Orientation | o | Integer\|String | Allowed values are `auto, 0, 90, -90, 180, -180, 270, -270` |
-| Quality | q | Integer | Can be restricted by parameter `AllowedQualities` |
+| Name | Shortcut | Type | Note |
+|---------------|----------|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| Original | original | - | A modifier without a value, use it if you want to return the original image |
+| Height | h | Integer | Can be restricted by parameter `AllowedResolutions` |
+| Width | w | Integer | Can be restricted by parameter `AllowedResolutions` |
+| Pixel density | pd | Integer or Float | Can be restricted by parameter `AllowedPixelDensity` |
+| Aspect ratio | ar | String | Required format is `{Int or Float}x{Int or Float}` and a height or a width (not both) must be also defined. For example `w:200,ar:1x2` is an equivalent of `w:200,h:400` |
+| Fit | f | String | See [supported fits](#supported-fits) for the list of supported values |
+| Orientation | o | Integer or String | Allowed values are `auto, 0, 90, -90, 180, -180, 270, -270` |
+| Quality | q | Integer | Can be restricted by parameter `AllowedQualities` |
### Supported fits
@@ -342,30 +343,11 @@ $ bin/console file-storage:clean [] [--namespace ] [--cache-only
### Local image server
-The default image server for each storage is `local`. That means your application will handle requests and will generate, store and serve modified images.
-Everything is prepared but the application must provide some endpoint. Here is an example of how to do it:
-
-```php
-addRoute('images/', 'Image:default');
-```
+The default image server for each storage is `local`. That means your application will handle requests and generate, store and serve modified images.
+The extension automatically registers [ImageStoragePresenter](src/Bridge/Nette/Application/ImageServerPresenter.php) and Routes for local storages if the `route: true` option is set for the storage.
+If you have this setting disabled, you must register the Presenter yourself.
-Then you must modify the configuration of a web server. For example, if the webserver is Apache then modify a file `.htaccess` that is located in your www directory.
+Now you must modify the configuration of a web server. For example, if the webserver is Apache then modify a file `.htaccess` that is located in your www directory.
```apacheconf
# locale images
@@ -376,11 +358,10 @@ RewriteRule ^(images\/)(.+) index.php [L]
The Application will be called only if a static file has not yet been generated. Otherwise, the server will serve the static file.
-
### External image server: an integration with AWS S3 and image-storage-lambda
The image storage can be integrated with the Amazon S3 object storage and the package [68publishers/image-storage-lambda](https://github.com/68publishers/image-storage-lambda). So your image storage can be completely serverless!
-Of course you can deploy the `image-storage-lambda` application manually and also synchronize options from the `image-storage` with the `image-storage-lambda` manually.
+Of course, you can deploy the `image-storage-lambda` application manually and also synchronize options from the `image-storage` with the `image-storage-lambda` manually.
At least you can follow these simple steps for a partial integration:
@@ -409,7 +390,7 @@ services:
config:
# configure what you want but omit the `host` option for now
filesystem:
- adapter: League\Flysystem\AwsS3V3\AwsS3V3Adapter(@s3_client, my-awesome-cache-bucket) # a bucket doesn't exists at this point
+ adapter: League\Flysystem\AwsS3V3\AwsS3V3Adapter(@s3_client, my-awesome-cache-bucket) # the bucket doesn't exists at this point
# if you have your own no-images:
assets:
%assetsDir%/noimage: noimage
@@ -418,7 +399,7 @@ services:
storages:
s3_images:
source_filesystem:
- adapter: League\Flysystem\AwsS3V3\AwsS3V3Adapter(@s3_client, my-awesome-source-bucket) # a bucket doesn't exists at this point
+ adapter: League\Flysystem\AwsS3V3\AwsS3V3Adapter(@s3_client, my-awesome-source-bucket) # the bucket doesn't exists at this point
server: external
# if you have your own no-images:
no_image:
@@ -428,25 +409,25 @@ services:
user: '^user_avatar\/'
```
-4) Register and configure a compiler extension `ImageStorageLambdaExtension`
+4) Register and configure the compiler extension `ImageStorageLambdaExtension`
```neon
extensions:
68publishers.image_storage.lambda: SixtyEightPublishers\ImageStorage\Bridge\Nette\DI\ImageStorageLambdaExtension
68publishers.image_storage.lambda:
- output_dir: %appDir%/config/image-storage-lambda # this is default
+ output_dir: %appDir%/config/image-storage-lambda # the default path
stacks:
s3_images:
- stack_name: my-awesome-image-storage
s3_bucket: {NAME OF YOUR DEPLOYMENT BUCKET FROM THE STEP 1}
region: eu-central-1
# optional settings:
+ stack_name: my-awesome-image-storage # the storage name is used by default
version: 2.0 # default is 1.0
- s3_prefix: custom-prefix # a stack_name is used by default
- confirm_changeset: yes # default false, must be changeset manually confirmed during deploy?
- capabilities: CAPABILITY_IAM # default, CAPABILITY_IAM or CAPABILITY_NAMED_IAM only
+ s3_prefix: custom-prefix # the stack_name is used by default
+ confirm_changeset: yes # must be changeset manually confirmed during deploy? the default value is false
+ capabilities: CAPABILITY_IAM # default, CAPABILITY_IAM or CAPABILITY_NAMED_IAM
# optional, automatically detected from AwsS3V3Adapter by default
source_bucket_name: source-bucket-name
@@ -455,7 +436,7 @@ extensions:
5) Generate configuration for the `image-storage-lambda`
-```bash
+```sh
$ php bin/console image-storage:lambda:dump-config
```
@@ -465,7 +446,7 @@ The configuration file will be placed by default in a directory `app/config/imag
Firstly setup your local environment by requirements defined [here](https://github.com/68publishers/image-storage-lambda#requirements). Then download the package outside your project.
-```bash
+```sh
$ git clone https://github.com/68publishers/image-storage-lambda.git image-storage-lambda
$ cd ./image-storage-lambda
```
@@ -496,14 +477,12 @@ The URL of your CloudFront distribution is listed in Outputs after a successful
## Contributing
-Before committing any changes, don't forget to run
-
-```bash
-vendor/bin/php-cs-fixer fix --config=.php_cs.dist -v --dry-run
-```
+Before opening a pull request, please check your changes using the following commands
-and
+```sh
+$ make init # to pull and start all docker images
-```bash
-vendor/bin/tester ./tests
+$ make cs.check
+$ make stan
+$ make tests.all
```
diff --git a/composer.json b/composer.json
index 5ab8c28..8456a82 100644
--- a/composer.json
+++ b/composer.json
@@ -27,13 +27,14 @@
"nette/application": "^3.1.8",
"nette/bootstrap": "^3.1",
"nette/di": "^3.0.10",
+ "nette/http": "^3.2.1",
"nette/tester": "^2.4.3",
"phpstan/phpstan": "^1.9",
"phpstan/phpstan-nette": "^1.1",
"roave/security-advisories": "dev-latest",
"symfony/console": "^5.0 | ^6.0",
"tracy/tracy": "^2.6",
- "yosymfony/toml": "^1.0"
+ "yosymfony/toml": "^1.0.4"
},
"suggest": {
"nette/di": "For an integration with Nette Framework.",
@@ -44,10 +45,12 @@
},
"conflict": {
"nette/di": "<3.0.10",
+ "nette/http": "<3.2.1",
"nette/schema": "<1.1",
"latte/latte": "<3.0",
"68publishers/doctrine-bridge": "<1.0.0",
- "symfony/console": "<5.0"
+ "symfony/console": "<5.0",
+ "yosymfony/toml": "<1.0.4"
},
"autoload": {
"psr-4": {
diff --git a/phpstan.neon b/phpstan.neon
index 20035f6..4946957 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -6,3 +6,9 @@ parameters:
level: 8
paths:
- src
+ ignoreErrors:
+ -
+ message: '#Parameter \$object of method SixtyEightPublishers\\ImageStorage\\Bridge\\Intervention\\Image\\Imagick\\Decoder::initFromImagick\(\) has invalid type Intervention\\Image\\Imagick\\Imagick.#'
+ path: src/Bridge/Intervention/Image/Imagick/Decoder.php
+ -
+ message: '#Return type \(Intervention\\Image\\Image\) of method [a-zA-Z\\]+::getSource\(\) should be compatible with return type \(resource\|string\) of method SixtyEightPublishers\\FileStorage\\Resource\\ResourceInterface::getSource\(\)#'
diff --git a/src/Bridge/ImageStorageLambda/Builder/ParameterOverrides.php b/src/Bridge/ImageStorageLambda/Builder/ParameterOverrides.php
deleted file mode 100644
index 4c1685f..0000000
--- a/src/Bridge/ImageStorageLambda/Builder/ParameterOverrides.php
+++ /dev/null
@@ -1,100 +0,0 @@
-= $this->count()) {
- return '';
- }
-
- $parameters = [];
-
- foreach ($this->parameters as $offset => $parameter) {
- if (is_array($parameter)) { # CommaDelimitedList
- $parameter = implode(',', $parameter);
- }
-
- $parameters[] = $offset . '="' . $parameter . '"';
- }
-
- return implode(' ', $parameters);
- }
-
- /**
- * {@inheritDoc}
- */
- public function offsetExists($offset): bool
- {
- return array_key_exists($offset, $this->parameters);
- }
-
- /**
- * {@inheritDoc}
- *
- * @throws \SixtyEightPublishers\ImageStorage\Exception\InvalidStateException
- */
- public function offsetGet($offset)
- {
- $this->checkOffsetExists($offset);
-
- return $this->parameters[$offset];
- }
-
- /**
- * {@inheritDoc}
- */
- public function offsetSet($offset, $value): void
- {
- $this->parameters[$offset] = $value;
- }
-
- /**
- * {@inheritDoc}
- *
- * @throws \SixtyEightPublishers\ImageStorage\Exception\InvalidStateException
- */
- public function offsetUnset($offset): void
- {
- $this->checkOffsetExists($offset);
- unset($this->parameters[$offset]);
- }
-
- /**
- * {@inheritDoc}
- */
- public function count(): int
- {
- return count($this->parameters);
- }
-
- /**
- * @param mixed $offset
- *
- * @return void
- * @throws \SixtyEightPublishers\ImageStorage\Exception\InvalidStateException
- */
- private function checkOffsetExists($offset): void
- {
- if (!$this->offsetExists($offset)) {
- throw new InvalidStateException(sprintf(
- 'Missing parameter with name %s',
- $offset
- ));
- }
- }
-}
diff --git a/src/Bridge/ImageStorageLambda/Builder/TomlConfigBuilder.php b/src/Bridge/ImageStorageLambda/Builder/TomlConfigBuilder.php
deleted file mode 100644
index 6dbdd9d..0000000
--- a/src/Bridge/ImageStorageLambda/Builder/TomlConfigBuilder.php
+++ /dev/null
@@ -1,171 +0,0 @@
-properties = $properties;
- }
-
- /**
- * {@inheritDoc}
- */
- public function buildToml(): TomlBuilder
- {
- $properties = $this->validateAndNormalizeProperties();
-
- # @todo: Workaround, related issue: https://github.com/yosymfony/toml/issues/29
- $toml = new class extends TomlBuilder {
- protected function dumpValue($val): string
- {
- if (is_float($val)) {
- $result = (string) $val;
-
- return $val != floor($val) ? $result : $result . '.0';
- }
-
- return parent::dumpValue($val);
- }
- };
-
- $toml->addComment(' Generated by 68publishers/image-storage')
- ->addValue('version', $properties->version)
- ->addTable('default.deploy.parameters')
- ->addValue('stack_name', $properties->stack_name)
- ->addValue('s3_bucket', $properties->s3_bucket)
- ->addValue('s3_prefix', $properties->s3_prefix)
- ->addValue('region', $properties->region)
- ->addValue('confirm_changeset', $properties->confirm_changeset)
- ->addValue('capabilities', $properties->capabilities);
-
- if (!empty($properties->parameter_overrides)) {
- $toml->addValue('parameter_overrides', $properties->parameter_overrides);
- }
-
- return $toml;
- }
-
- /**
- * {@inheritDoc}
- */
- public function withProperty(string $name, $value): TomlConfigBuilderInterface
- {
- $this->properties[$name] = $value;
-
- return $this;
- }
-
- /**
- * @param float $version
- *
- * @return \SixtyEightPublishers\ImageStorage\Bridge\ImageStorageLambda\Builder\TomlConfigBuilderInterface
- */
- public function setVersion(float $version): TomlConfigBuilderInterface
- {
- return $this->withProperty('version', $version);
- }
-
- /**
- * @param string $stackName
- *
- * @return \SixtyEightPublishers\ImageStorage\Bridge\ImageStorageLambda\Builder\TomlConfigBuilderInterface
- */
- public function setStackName(string $stackName): TomlConfigBuilderInterface
- {
- return $this->withProperty('stack_name', $stackName);
- }
-
- /**
- * @param string $s3Bucket
- *
- * @return \SixtyEightPublishers\ImageStorage\Bridge\ImageStorageLambda\Builder\TomlConfigBuilderInterface
- */
- public function setS3Bucket(string $s3Bucket): TomlConfigBuilderInterface
- {
- return $this->withProperty('s3_bucket', $s3Bucket);
- }
-
- /**
- * @param string $s3Prefix
- *
- * @return \SixtyEightPublishers\ImageStorage\Bridge\ImageStorageLambda\Builder\TomlConfigBuilderInterface
- */
- public function setS3Prefix(string $s3Prefix): TomlConfigBuilderInterface
- {
- return $this->withProperty('s3_prefix', $s3Prefix);
- }
-
- /**
- * @param string $region
- *
- * @return \SixtyEightPublishers\ImageStorage\Bridge\ImageStorageLambda\Builder\TomlConfigBuilderInterface
- */
- public function setRegion(string $region): TomlConfigBuilderInterface
- {
- return $this->withProperty('region', $region);
- }
-
- /**
- * @param bool $confirmChangeSet
- *
- * @return \SixtyEightPublishers\ImageStorage\Bridge\ImageStorageLambda\Builder\TomlConfigBuilderInterface
- */
- public function setConfirmChangeSet(bool $confirmChangeSet): TomlConfigBuilderInterface
- {
- return $this->withProperty('confirm_changeset', $confirmChangeSet);
- }
-
- /**
- * @param string $capabilities
- *
- * @return \SixtyEightPublishers\ImageStorage\Bridge\ImageStorageLambda\Builder\TomlConfigBuilderInterface
- */
- public function setCapabilities(string $capabilities): TomlConfigBuilderInterface
- {
- return $this->withProperty('capabilities', $capabilities);
- }
-
- /**
- * @param string|\SixtyEightPublishers\ImageStorage\Bridge\ImageStorageLambda\Builder\ParameterOverrides $parameterOverrides
- *
- * @return \SixtyEightPublishers\ImageStorage\Bridge\ImageStorageLambda\Builder\TomlConfigBuilderInterface
- */
- public function setParameterOverrides($parameterOverrides): TomlConfigBuilderInterface
- {
- return $this->withProperty('parameter_overrides', $parameterOverrides);
- }
-
- /**
- * @return object
- */
- private function validateAndNormalizeProperties(): object
- {
- $processor = new Processor();
- $schema = Expect::structure([
- 'version' => Expect::float(1.0),
- 'stack_name' => Expect::string()->required(),
- 's3_bucket' => Expect::string()->required(),
- 's3_prefix' => Expect::string()->required(),
- 'region' => Expect::string()->required(),
- 'confirm_changeset' => Expect::bool(false),
- 'capabilities' => Expect::anyOf(self::CAPABILITY_IAM, self::CAPABILITY_NAMED_IAM)->default(self::CAPABILITY_IAM),
- 'parameter_overrides' => Expect::type('string|' . ParameterOverrides::class)->castTo('string'),
- ]);
-
- return $processor->process($schema, $this->properties);
- }
-}
diff --git a/src/Bridge/ImageStorageLambda/Builder/TomlConfigBuilderFactoryInterface.php b/src/Bridge/ImageStorageLambda/Builder/TomlConfigBuilderFactoryInterface.php
deleted file mode 100644
index f13a143..0000000
--- a/src/Bridge/ImageStorageLambda/Builder/TomlConfigBuilderFactoryInterface.php
+++ /dev/null
@@ -1,10 +0,0 @@
-|stdClass $values
+ *
+ * @return static
+ */
+ public static function fromValues(array|stdClass $values): self
+ {
+ $config = (new Processor())->process(self::createSchema(), (array) $values);
+ assert($config instanceof self);
+
+ return $config;
+ }
+
+ public static function createSchema(): Schema
+ {
+ $schema = Expect::structure([
+ 'stack_name' => Expect::string()
+ ->nullable()
+ ->dynamic(),
+ 'version' => Expect::float(1.0)
+ ->dynamic(),
+ 's3_bucket' => Expect::string()
+ ->required()
+ ->dynamic(),
+ 's3_prefix' => Expect::string()
+ ->nullable()
+ ->dynamic(),
+ 'region' => Expect::string()
+ ->required()
+ ->dynamic(),
+ 'confirm_changeset' => Expect::bool(false)
+ ->dynamic(),
+ 'capabilities' => Expect::anyOf(self::CAPABILITY_IAM, self::CAPABILITY_NAMED_IAM)
+ ->default(self::CAPABILITY_IAM)
+ ->dynamic(),
+ 'parameter_overrides' => Expect::anyOf(
+ Expect::type(ParameterOverrides::class),
+ Expect::arrayOf(
+ Expect::anyOf(
+ Expect::scalar(),
+ Expect::listOf(Expect::scalar())
+ ),
+ 'string'
+ )
+ )->default(new ParameterOverrides([]))
+ ->before(static fn (ParameterOverrides|array $value): ParameterOverrides => is_array($value) ? new ParameterOverrides($value) : $value),
+ 'source_bucket_name' => Expect::string()
+ ->nullable()
+ ->dynamic(), # detected automatically from AwsS3V3Adapter by default
+ 'cache_bucket_name' => Expect::string()
+ ->nullable()
+ ->dynamic(), # detected automatically from AwsS3V3Adapter by default
+ ]);
+
+ $schema->before(static function (array $config): array {
+ if (empty($config['s3_prefix'] ?? '') && !empty($config['stack_name'] ?? '')) {
+ $config['s3_prefix'] = $config['stack_name'];
+ }
+
+ return $config;
+ });
+
+ return $schema->castTo(self::class);
+ }
+
+ /**
+ * @return array
+ */
+ public function toArray(): array
+ {
+ return [
+ 'stack_name' => $this->stack_name,
+ 'version' => $this->version,
+ 's3_bucket' => $this->s3_bucket,
+ 's3_prefix' => $this->s3_prefix,
+ 'region' => $this->region,
+ 'confirm_changeset' => $this->confirm_changeset,
+ 'capabilities' => $this->capabilities,
+ 'parameter_overrides' => $this->parameter_overrides->parameters,
+ 'source_bucket_name' => $this->source_bucket_name,
+ 'cache_bucket_name' => $this->cache_bucket_name,
+ ];
+ }
+}
diff --git a/src/Bridge/ImageStorageLambda/ParameterOverrides.php b/src/Bridge/ImageStorageLambda/ParameterOverrides.php
new file mode 100644
index 0000000..9a309c5
--- /dev/null
+++ b/src/Bridge/ImageStorageLambda/ParameterOverrides.php
@@ -0,0 +1,57 @@
+> $parameters
+ */
+ public function __construct(
+ public readonly array $parameters,
+ ) {
+ }
+
+ /**
+ * @param array> $parameters
+ */
+ public function withMissingParameters(array $parameters): self
+ {
+ $newParameters = $this->parameters;
+
+ foreach ($parameters as $offset => $parameter) {
+ if (!array_key_exists($offset, $newParameters)) {
+ $newParameters[$offset] = $parameter;
+ }
+ }
+
+ return new self($newParameters);
+ }
+
+ public function __toString(): string
+ {
+ if (0 >= count($this->parameters)) {
+ return '';
+ }
+
+ $parameters = [];
+
+ foreach ($this->parameters as $offset => $parameter) {
+ if (is_array($parameter)) { # CommaDelimitedList
+ $parameter = implode(',', $parameter);
+ }
+
+ $parameters[] = $offset . '="' . $parameter . '"';
+ }
+
+ return implode(' ', $parameters);
+ }
+}
diff --git a/src/Bridge/ImageStorageLambda/SamConfigGenerator.php b/src/Bridge/ImageStorageLambda/SamConfigGenerator.php
index f05f4ea..02abdd2 100644
--- a/src/Bridge/ImageStorageLambda/SamConfigGenerator.php
+++ b/src/Bridge/ImageStorageLambda/SamConfigGenerator.php
@@ -5,149 +5,91 @@
namespace SixtyEightPublishers\ImageStorage\Bridge\ImageStorageLambda;
use ReflectionProperty;
-use ReflectionException;
+use Yosymfony\Toml\TomlBuilder;
use League\Flysystem\FilesystemOperator;
use League\Flysystem\AwsS3V3\AwsS3V3Adapter;
use SixtyEightPublishers\ImageStorage\Config\Config;
+use SixtyEightPublishers\FileStorage\Config\ConfigInterface;
use SixtyEightPublishers\ImageStorage\ImageStorageInterface;
use SixtyEightPublishers\ImageStorage\Exception\InvalidStateException;
use SixtyEightPublishers\ImageStorage\Exception\InvalidArgumentException;
use SixtyEightPublishers\ImageStorage\Filesystem\AdapterProviderInterface;
use SixtyEightPublishers\ImageStorage\Persistence\ImagePersisterInterface;
-use SixtyEightPublishers\ImageStorage\Bridge\ImageStorageLambda\Stack\StackInterface;
-use SixtyEightPublishers\ImageStorage\Bridge\ImageStorageLambda\Builder\ParameterOverrides;
-use SixtyEightPublishers\ImageStorage\Bridge\ImageStorageLambda\Builder\TomlConfigBuilderFactoryInterface;
+use function count;
+use function floor;
+use function mkdir;
+use function rtrim;
+use function assert;
+use function is_dir;
+use function dirname;
+use function sprintf;
+use function is_float;
+use function realpath;
+use function is_string;
+use function array_keys;
+use function array_merge;
+use function array_filter;
+use function file_put_contents;
final class SamConfigGenerator implements SamConfigGeneratorInterface
{
- /** @var \SixtyEightPublishers\ImageStorage\Bridge\ImageStorageLambda\Builder\TomlConfigBuilderFactoryInterface */
- private $tomlConfigBuilderFactory;
-
- /** @var string */
- private $outputDir;
-
- /** @var \SixtyEightPublishers\ImageStorage\Bridge\ImageStorageLambda\Stack\StackInterface[] */
- private $stacks;
-
/**
- * @param \SixtyEightPublishers\ImageStorage\Bridge\ImageStorageLambda\Builder\TomlConfigBuilderFactoryInterface $tomlConfigBuilderFactory
- * @param string $outputDir
- * @param array $stacks
+ * @param array> $configs
*/
- public function __construct(TomlConfigBuilderFactoryInterface $tomlConfigBuilderFactory, string $outputDir, array $stacks)
- {
- $this->tomlConfigBuilderFactory = $tomlConfigBuilderFactory;
- $this->outputDir = $outputDir;
-
- foreach ($stacks as $stack) {
- $this->addStack($stack);
- }
+ public function __construct(
+ private readonly string $outputDir,
+ private readonly array $configs,
+ ) {
}
- /**
- * {@inheritDoc}
- */
- public function hasStackForStorage(ImageStorageInterface $imageStorage): bool
+ public function canGenerate(ImageStorageInterface $imageStorage): bool
{
- return isset($this->stacks[$imageStorage->getName()]);
+ return isset($this->configs[$imageStorage->getName()]);
}
/**
- * {@inheritDoc}
- *
- * @throws ReflectionException
+ * @throws \SixtyEightPublishers\ImageStorage\Exception\InvalidArgumentException
*/
- public function generateForStorage(ImageStorageInterface $imageStorage): string
+ public function generate(ImageStorageInterface $imageStorage): string
{
- $stack = $this->stacks[$imageStorage->getName()] ?? null;
+ $lambdaConfig = $this->configs[$imageStorage->getName()] ?? null;
- if (null === $stack) {
+ if (null === $lambdaConfig) {
throw new InvalidArgumentException(sprintf(
- 'Missing stack with name "%s".',
+ 'Missing config with the name "%s".',
$imageStorage->getName()
));
}
- # Bucket names
+ $lambdaConfig = LambdaConfig::fromValues($lambdaConfig);
$buckets = [
- ImagePersisterInterface::FILESYSTEM_NAME_SOURCE => $stack->getSourceBucketName(),
- ImagePersisterInterface::FILESYSTEM_NAME_CACHE => $stack->getCacheBucketName(),
+ ImagePersisterInterface::FILESYSTEM_NAME_SOURCE => $lambdaConfig->source_bucket_name,
+ ImagePersisterInterface::FILESYSTEM_NAME_CACHE => $lambdaConfig->cache_bucket_name,
];
-
- $missingBuckets = array_keys(array_filter($buckets, static function ($bucketName) {
- return empty($bucketName);
- }));
+ $missingBuckets = array_keys(array_filter($buckets, static fn (?string $bucketName): bool => null === $bucketName));
if (0 < count($missingBuckets)) {
- $buckets = array_merge($buckets, $this->detectBucketNamesFromFilesystem($imageStorage->getFilesystem(), $missingBuckets));
- }
-
- # Create TOML builder and fill it with configured properties
- $builder = $this->tomlConfigBuilderFactory->create();
-
- foreach ($stack->getValues() as $k => $v) {
- $builder->withProperty((string) $k, $v);
+ $buckets = array_merge(
+ $buckets,
+ $this->detectBucketNamesFromFilesystem($imageStorage->getFilesystem(), $missingBuckets)
+ );
}
- # Create a "parameter_overrides" property
- $parameterOverrides = new ParameterOverrides();
- $config = $imageStorage->getConfig();
- $noImageConfig = $imageStorage->getNoImageConfig();
- $noImages = $noImagePatterns = [];
+ assert(is_string($buckets[ImagePersisterInterface::FILESYSTEM_NAME_SOURCE]) && is_string($buckets[ImagePersisterInterface::FILESYSTEM_NAME_CACHE]));
- if (null !== $noImageConfig->getDefaultPath()) {
- $noImages[] = 'default::' . $noImageConfig->getDefaultPath();
- }
-
- foreach ($noImageConfig->getPaths() as $noImageName => $path) {
- $noImages[] = $noImageName . '::' . $path;
- }
-
- foreach ($noImageConfig->getPatterns() as $noImageName => $pattern) {
- $noImagePatterns[] = $noImageName . '::' . $pattern;
- }
+ $lambdaConfig->parameter_overrides = $lambdaConfig->parameter_overrides->withMissingParameters(
+ $this->createDefaultParameterOverrides(
+ $imageStorage,
+ $buckets[ImagePersisterInterface::FILESYSTEM_NAME_SOURCE],
+ $buckets[ImagePersisterInterface::FILESYSTEM_NAME_CACHE]
+ )
+ );
- $parameterOverrides['BasePath'] = $config[Config::BASE_PATH];
- $parameterOverrides['ModifierSeparator'] = $config[Config::MODIFIER_SEPARATOR];
- $parameterOverrides['ModifierAssigner'] = $config[Config::MODIFIER_ASSIGNER];
- $parameterOverrides['VersionParameterName'] = $config[Config::VERSION_PARAMETER_NAME];
- $parameterOverrides['SignatureParameterName'] = $config[Config::SIGNATURE_PARAMETER_NAME];
- $parameterOverrides['SignatureKey'] = $config[Config::SIGNATURE_KEY];
- $parameterOverrides['SignatureAlgorithm'] = $config[Config::SIGNATURE_ALGORITHM];
- $parameterOverrides['AllowedPixelDensity'] = $config[Config::ALLOWED_PIXEL_DENSITY];
- $parameterOverrides['AllowedResolutions'] = $config[Config::ALLOWED_RESOLUTIONS];
- $parameterOverrides['AllowedQualities'] = $config[Config::ALLOWED_QUALITIES];
- $parameterOverrides['EncodeQuality'] = $config[Config::ENCODE_QUALITY];
- $parameterOverrides['SourceBucketName'] = $buckets[ImagePersisterInterface::FILESYSTEM_NAME_SOURCE];
- $parameterOverrides['CacheBucketName'] = $buckets[ImagePersisterInterface::FILESYSTEM_NAME_CACHE];
- $parameterOverrides['CacheMaxAge'] = $config[Config::CACHE_MAX_AGE];
- $parameterOverrides['NoImages'] = $noImages;
- $parameterOverrides['NoImagePatterns'] = $noImagePatterns;
-
- $builder->setParameterOverrides($parameterOverrides);
-
- # Build and write it!
- $toml = $builder->buildToml();
-
- return $this->write($toml->getTomlString(), $stack->getName());
- }
+ $toml = $this->createToml($lambdaConfig, $imageStorage->getName());
- /**
- * @param \SixtyEightPublishers\ImageStorage\Bridge\ImageStorageLambda\Stack\StackInterface $stack
- *
- * @return void
- */
- private function addStack(StackInterface $stack): void
- {
- $this->stacks[$stack->getName()] = $stack;
+ return $this->write($toml, $lambdaConfig->stack_name ?? $imageStorage->getName());
}
- /**
- * @param string $content
- * @param string $name
- *
- * @return string
- */
private function write(string $content, string $name): string
{
$filename = sprintf(
@@ -176,21 +118,15 @@ private function write(string $content, string $name): string
}
/**
- * @param \League\Flysystem\FilesystemOperator $filesystemOperator
- * @param array $prefixes
+ * @param array $prefixes
*
- * @return array
- * @throws ReflectionException
+ * @return array
*/
private function detectBucketNamesFromFilesystem(FilesystemOperator $filesystemOperator, array $prefixes): array
{
- if (empty($prefixes)) {
- return [];
- }
-
if (!$filesystemOperator instanceof AdapterProviderInterface) {
throw new InvalidStateException(sprintf(
- 'Can\'t detect bucket names from a filesystem because the filesystem must be implementor of %s',
+ 'Can\'t detect bucket names from a filesystem because the filesystem must be an implementor of %s.',
AdapterProviderInterface::class
));
}
@@ -202,18 +138,97 @@ private function detectBucketNamesFromFilesystem(FilesystemOperator $filesystemO
if (!$adapter instanceof AwsS3V3Adapter) {
throw new InvalidStateException(sprintf(
- 'Adapter must be instance of %s.',
+ 'Adapter must be an instance of %s.',
AwsS3V3Adapter::class
));
}
$reflectionProperty = new ReflectionProperty(AwsS3V3Adapter::class, 'bucket');
+ $bucket = $reflectionProperty->getValue($adapter);
+ assert(is_string($bucket));
- $reflectionProperty->setAccessible(true);
-
- $buckets[$prefix] = $reflectionProperty->getValue($adapter);
+ $buckets[$prefix] = $bucket;
}
return $buckets;
}
+
+ /**
+ * @return array>
+ */
+ private function createDefaultParameterOverrides(ImageStorageInterface $imageStorage, string $sourceBucketName, string $cacheBucketName): array
+ {
+ $imageStorageConfig = $imageStorage->getConfig();
+ $noImageConfig = $imageStorage->getNoImageConfig();
+ $noImages = $noImagePatterns = [];
+
+ if (null !== $noImageConfig->getDefaultPath()) {
+ $noImages[] = 'default::' . $noImageConfig->getDefaultPath();
+ }
+
+ foreach ($noImageConfig->getPaths() as $noImageName => $path) {
+ $noImages[] = $noImageName . '::' . $path;
+ }
+
+ foreach ($noImageConfig->getPatterns() as $noImageName => $pattern) {
+ $noImagePatterns[] = $noImageName . '::' . $pattern;
+ }
+
+ return [
+ 'BasePath' => $imageStorageConfig[ConfigInterface::BASE_PATH],
+ 'ModifierSeparator' => $imageStorageConfig[Config::MODIFIER_SEPARATOR],
+ 'ModifierAssigner' => $imageStorageConfig[Config::MODIFIER_ASSIGNER],
+ 'VersionParameterName' => $imageStorageConfig[ConfigInterface::VERSION_PARAMETER_NAME],
+ 'SignatureParameterName' => $imageStorageConfig[Config::SIGNATURE_PARAMETER_NAME],
+ 'SignatureKey' => $imageStorageConfig[Config::SIGNATURE_KEY],
+ 'SignatureAlgorithm' => $imageStorageConfig[Config::SIGNATURE_ALGORITHM],
+ 'AllowedPixelDensity' => $imageStorageConfig[Config::ALLOWED_PIXEL_DENSITY],
+ 'AllowedResolutions' => $imageStorageConfig[Config::ALLOWED_RESOLUTIONS],
+ 'AllowedQualities' => $imageStorageConfig[Config::ALLOWED_QUALITIES],
+ 'EncodeQuality' => $imageStorageConfig[Config::ENCODE_QUALITY],
+ 'SourceBucketName' => $sourceBucketName,
+ 'CacheBucketName' => $cacheBucketName,
+ 'CacheMaxAge' => $imageStorageConfig[Config::CACHE_MAX_AGE],
+ 'NoImages' => $noImages,
+ 'NoImagePatterns' => $noImagePatterns,
+ ];
+ }
+
+ private function createToml(LambdaConfig $lambdaConfig, string $imageStorageName): string
+ {
+ # @todo: Workaround, related issue: https://github.com/yosymfony/toml/issues/29 ... still not released (2.1.2023)
+ $toml = new class extends TomlBuilder {
+ /**
+ * @param string|int|bool|float|array $val
+ */
+ protected function dumpValue($val): string
+ {
+ if (is_float($val)) {
+ $result = (string) $val;
+
+ return $val !== floor($val) ? $result : $result . '.0';
+ }
+
+ return parent::dumpValue($val);
+ }
+ };
+
+ $toml->addComment(' Generated by 68publishers/image-storage')
+ ->addValue('version', $lambdaConfig->version)
+ ->addTable('default.deploy.parameters')
+ ->addValue('stack_name', $lambdaConfig->stack_name ?? $imageStorageName)
+ ->addValue('s3_bucket', $lambdaConfig->s3_bucket)
+ ->addValue('s3_prefix', $lambdaConfig->s3_prefix ?? $imageStorageName)
+ ->addValue('region', $lambdaConfig->region)
+ ->addValue('confirm_changeset', $lambdaConfig->confirm_changeset)
+ ->addValue('capabilities', $lambdaConfig->capabilities);
+
+ $parameterOverrides = (string) $lambdaConfig->parameter_overrides;
+
+ if (!empty($parameterOverrides)) {
+ $toml->addValue('parameter_overrides', $parameterOverrides);
+ }
+
+ return $toml->getTomlString();
+ }
}
diff --git a/src/Bridge/ImageStorageLambda/SamConfigGeneratorInterface.php b/src/Bridge/ImageStorageLambda/SamConfigGeneratorInterface.php
index 586aeaa..6095253 100644
--- a/src/Bridge/ImageStorageLambda/SamConfigGeneratorInterface.php
+++ b/src/Bridge/ImageStorageLambda/SamConfigGeneratorInterface.php
@@ -8,10 +8,10 @@
interface SamConfigGeneratorInterface
{
- public function hasStackForStorage(ImageStorageInterface $imageStorage): bool;
+ public function canGenerate(ImageStorageInterface $imageStorage): bool;
/**
* Returns path to the generated config
*/
- public function generateForStorage(ImageStorageInterface $imageStorage): string;
+ public function generate(ImageStorageInterface $imageStorage): string;
}
diff --git a/src/Bridge/ImageStorageLambda/Stack/Stack.php b/src/Bridge/ImageStorageLambda/Stack/Stack.php
deleted file mode 100644
index 27bd08e..0000000
--- a/src/Bridge/ImageStorageLambda/Stack/Stack.php
+++ /dev/null
@@ -1,39 +0,0 @@
- $values
- */
- public function __construct(
- private readonly string $name,
- private readonly array $values,
- private readonly ?string $sourceBucketName = null,
- private readonly ?string $cacheBucketName = null
- ) {
- }
-
- public function getName(): string
- {
- return $this->name;
- }
-
- public function getValues(): array
- {
- return $this->values;
- }
-
- public function getSourceBucketName(): ?string
- {
- return $this->sourceBucketName;
- }
-
- public function getCacheBucketName(): ?string
- {
- return $this->cacheBucketName;
- }
-}
diff --git a/src/Bridge/ImageStorageLambda/Stack/StackInterface.php b/src/Bridge/ImageStorageLambda/Stack/StackInterface.php
deleted file mode 100644
index 9e02cb6..0000000
--- a/src/Bridge/ImageStorageLambda/Stack/StackInterface.php
+++ /dev/null
@@ -1,19 +0,0 @@
-
- */
- public function getValues(): array;
-
- public function getSourceBucketName(): ?string;
-
- public function getCacheBucketName(): ?string;
-}
diff --git a/src/Bridge/Intervention/Image/AbstractCommandExecutor.php b/src/Bridge/Intervention/Image/AbstractCommandExecutor.php
index 839d768..0a11390 100644
--- a/src/Bridge/Intervention/Image/AbstractCommandExecutor.php
+++ b/src/Bridge/Intervention/Image/AbstractCommandExecutor.php
@@ -7,6 +7,7 @@
use Intervention\Image\Image;
use Intervention\Image\Commands\AbstractCommand;
use Intervention\Image\Exception\NotSupportedException;
+use function assert;
use function substr;
use function sprintf;
use function ucfirst;
@@ -77,6 +78,9 @@ protected function getCommandClassName(string $name): string
*/
protected function createCommand(string $className, array $arguments): AbstractCommand
{
- return new $className($arguments);
+ $command = new $className($arguments);
+ assert($command instanceof AbstractCommand);
+
+ return $command;
}
}
diff --git a/src/Bridge/Intervention/Image/CommandExecutorInterface.php b/src/Bridge/Intervention/Image/CommandExecutorInterface.php
index 9d8a021..0da49cf 100644
--- a/src/Bridge/Intervention/Image/CommandExecutorInterface.php
+++ b/src/Bridge/Intervention/Image/CommandExecutorInterface.php
@@ -10,7 +10,7 @@
interface CommandExecutorInterface
{
/**
- * @param array $arguments
+ * @param array $arguments
*/
public function execute(Image $image, string $name, array $arguments): AbstractCommand;
}
diff --git a/src/Bridge/Intervention/Image/DriverProxy.php b/src/Bridge/Intervention/Image/DriverProxy.php
index 5c1caac..db5db92 100644
--- a/src/Bridge/Intervention/Image/DriverProxy.php
+++ b/src/Bridge/Intervention/Image/DriverProxy.php
@@ -17,6 +17,9 @@ public function __construct(
) {
}
+ /**
+ * @param array $arguments
+ */
public function executeCommand($image, $name, $arguments): AbstractCommand
{
return $this->commandExecutor->execute($image, $name, $arguments);
diff --git a/src/Bridge/Latte/ImageStorageFunctions.php b/src/Bridge/Latte/ImageStorageFunctions.php
deleted file mode 100644
index 27a275e..0000000
--- a/src/Bridge/Latte/ImageStorageFunctions.php
+++ /dev/null
@@ -1,120 +0,0 @@
- 'w_descriptor',
- self::FUNCTION_ID_CREATE_X_DESCRIPTOR => 'x_descriptor',
- self::FUNCTION_ID_CREATE_W_DESCRIPTOR_FROM_RANGE => 'w_descriptor_range',
- self::FUNCTION_ID_CREATE_NO_IMAGE => 'no_image',
- ];
-
- private const FUNCTION_CALLBACKS = [
- self::FUNCTION_ID_CREATE_W_DESCRIPTOR => 'createWDescriptor',
- self::FUNCTION_ID_CREATE_X_DESCRIPTOR => 'createXDescriptor',
- self::FUNCTION_ID_CREATE_W_DESCRIPTOR_FROM_RANGE => 'createWDescriptorFromRange',
- self::FUNCTION_ID_CREATE_NO_IMAGE => 'createNoImage',
- ];
-
- /** @var \SixtyEightPublishers\FileStorage\FileStorageProviderInterface */
- private $fileStorageProvider;
-
- /**
- * @param \SixtyEightPublishers\FileStorage\FileStorageProviderInterface $fileStorageProvider
- */
- public function __construct(FileStorageProviderInterface $fileStorageProvider)
- {
- $this->fileStorageProvider = $fileStorageProvider;
- }
-
- /**
- * @param \SixtyEightPublishers\FileStorage\FileStorageProviderInterface $fileStorageProvider
- * @param \Latte\Engine $engine
- * @param array $customFunctionNames
- *
- * @return void
- */
- public static function register(FileStorageProviderInterface $fileStorageProvider, Engine $engine, array $customFunctionNames = []): void
- {
- $me = new static($fileStorageProvider);
-
- foreach (array_merge(self::DEFAULT_FUNCTION_NAMES, $customFunctionNames) as $functionId => $functionName) {
- $engine->addFunction($functionName, [$me, self::FUNCTION_CALLBACKS[$functionId]]);
- }
- }
-
- /**
- * @param int ...$widths
- *
- * @return \SixtyEightPublishers\ImageStorage\Responsive\Descriptor\DescriptorInterface
- */
- public function createWDescriptor(int ...$widths): DescriptorInterface
- {
- return new WDescriptor(...$widths);
- }
-
- /**
- * @param int $min
- * @param int $max
- * @param int $step
- *
- * @return \SixtyEightPublishers\ImageStorage\Responsive\Descriptor\DescriptorInterface
- */
- public function createWDescriptorFromRange(int $min, int $max, int $step = 100): DescriptorInterface
- {
- return WDescriptor::fromRange($min, $max, $step);
- }
-
- /**
- * @param mixed ...$pixelDensities
- *
- * @return \SixtyEightPublishers\ImageStorage\Responsive\Descriptor\DescriptorInterface
- */
- public function createXDescriptor(...$pixelDensities): DescriptorInterface
- {
- if (empty($pixelDensities)) {
- return XDescriptor::default();
- }
-
- return new XDescriptor(...$pixelDensities);
- }
-
- /**
- * @param string|NULL $noImageName
- * @param string|NULL $storageName
- *
- * @return \SixtyEightPublishers\ImageStorage\FileInfoInterface
- */
- public function createNoImage(?string $noImageName = null, ?string $storageName = null): ImageFileInfoInterface
- {
- $storage = $this->fileStorageProvider->get($storageName);
-
- if (!$storage instanceof ImageStorageInterface) {
- throw new InvalidArgumentException(sprintf(
- 'Storage "%s" must be implementor of %s.',
- $storage->getName(),
- ImageStorageInterface::class
- ));
- }
-
- return $storage->createFileInfo($storage->getNoImage($noImageName));
- }
-}
diff --git a/src/Bridge/Latte/ImageStorageLatteExtension.php b/src/Bridge/Latte/ImageStorageLatteExtension.php
new file mode 100644
index 0000000..f0c1795
--- /dev/null
+++ b/src/Bridge/Latte/ImageStorageLatteExtension.php
@@ -0,0 +1,46 @@
+ static fn (int ...$widths): WDescriptor => new WDescriptor(...$widths),
+ 'w_descriptor_range' => static fn (int $min, int $max, int $step = 100): WDescriptor => WDescriptor::fromRange($min, $max, $step),
+ 'x_descriptor' => static fn (...$pixelDensities): XDescriptor => empty($pixelDensities) ? XDescriptor::default() : new XDescriptor(...$pixelDensities),
+ 'no_image' => fn (?string $noImageName = null, ?string $storageName = null) => $this->createNoImage($noImageName, $storageName),
+ ];
+ }
+
+ private function createNoImage(?string $noImageName = null, ?string $storageName = null): ImageFileInfoInterface
+ {
+ $storage = $this->fileStorageProvider->get($storageName);
+
+ if (!$storage instanceof ImageStorageInterface) {
+ throw new InvalidArgumentException(sprintf(
+ 'Storage "%s" must be implementor of %s.',
+ $storage->getName(),
+ ImageStorageInterface::class
+ ));
+ }
+
+ return $storage->createFileInfo($storage->getNoImage($noImageName));
+ }
+}
diff --git a/src/Bridge/Nette/Application/ImageServerPresenter.php b/src/Bridge/Nette/Application/ImageServerPresenter.php
new file mode 100644
index 0000000..6b80ac4
--- /dev/null
+++ b/src/Bridge/Nette/Application/ImageServerPresenter.php
@@ -0,0 +1,70 @@
+getParameter('__storageName');
+ $storage = $this->fileStorageProvider->get(is_string($storageName) ? $storageName : null);
+
+ if (!$storage instanceof ImageStorageInterface) {
+ throw new InvalidStateException(sprintf(
+ 'File storage "%s" must be implementor of an interface %s.',
+ $storage->getName(),
+ ImageStorageInterface::class
+ ));
+ }
+
+ $response = $storage->getImageResponse(new Request($this->request));
+ assert($response instanceof ApplicationResponse);
+
+ if ($response instanceof ErrorResponse) {
+ $this->logErrorResponse($response);
+ }
+
+ return $response;
+ }
+
+ private function logErrorResponse(ErrorResponse $response): void
+ {
+ if (null !== $this->logger) {
+ $this->logger->error($response->getException()->getMessage(), [
+ 'exception' => $response->getException(),
+ ]);
+
+ return;
+ }
+
+ if (class_exists(Debugger::class)) {
+ Debugger::log($response->getException(), ILogger::ERROR);
+ }
+ }
+}
diff --git a/src/Bridge/Nette/Application/ImageServerRoute.php b/src/Bridge/Nette/Application/ImageServerRoute.php
new file mode 100644
index 0000000..87dfed2
--- /dev/null
+++ b/src/Bridge/Nette/Application/ImageServerRoute.php
@@ -0,0 +1,21 @@
+', [
+ 'module' => 'ImageStorage',
+ 'presenter' => 'ImageServer',
+ 'action' => 'default',
+ '__storageName' => $storageName,
+ ]);
+ }
+}
diff --git a/src/Bridge/Nette/DI/Config/ImageStorageConfig.php b/src/Bridge/Nette/DI/Config/ImageStorageConfig.php
new file mode 100644
index 0000000..4593b11
--- /dev/null
+++ b/src/Bridge/Nette/DI/Config/ImageStorageConfig.php
@@ -0,0 +1,15 @@
+ */
+ public array $storages;
+}
diff --git a/src/Bridge/Nette/DI/Config/ImageStorageLambdaConfig.php b/src/Bridge/Nette/DI/Config/ImageStorageLambdaConfig.php
new file mode 100644
index 0000000..fe32b1c
--- /dev/null
+++ b/src/Bridge/Nette/DI/Config/ImageStorageLambdaConfig.php
@@ -0,0 +1,15 @@
+ */
+ public array $stacks;
+}
diff --git a/src/Bridge/Nette/DI/Config/StorageConfig.php b/src/Bridge/Nette/DI/Config/StorageConfig.php
new file mode 100644
index 0000000..e8a2a2c
--- /dev/null
+++ b/src/Bridge/Nette/DI/Config/StorageConfig.php
@@ -0,0 +1,35 @@
+ */
+ public array $no_image;
+
+ /** @var array */
+ public array $no_image_patterns;
+
+ /** @var array> */
+ public array $presets;
+
+ /** @var array */
+ public array $modifiers;
+
+ /** @var array */
+ public array $applicators;
+
+ /** @var array */
+ public array $validators;
+}
diff --git a/src/Bridge/Nette/DI/ImageStorageExtension.php b/src/Bridge/Nette/DI/ImageStorageExtension.php
index 0f9a35a..ee321d8 100644
--- a/src/Bridge/Nette/DI/ImageStorageExtension.php
+++ b/src/Bridge/Nette/DI/ImageStorageExtension.php
@@ -8,10 +8,12 @@
use Nette\Schema\Schema;
use Nette\DI\CompilerExtension;
use League\Flysystem\Visibility;
+use Nette\DI\Definitions\Reference;
use Nette\DI\Definitions\Statement;
use Intervention\Image\ImageManager;
-use Nette\DI\Definitions\Definition;
use League\Flysystem\FilesystemOperator;
+use Nette\Application\IPresenterFactory;
+use Nette\DI\Definitions\ServiceDefinition;
use League\Flysystem\Config as FlysystemConfig;
use SixtyEightPublishers\ImageStorage\Modifier;
use SixtyEightPublishers\ImageStorage\ImageStorage;
@@ -41,24 +43,35 @@
use SixtyEightPublishers\ImageStorage\ImageServer\LocalImageServerFactory;
use SixtyEightPublishers\ImageStorage\Persistence\ImagePersisterInterface;
use SixtyEightPublishers\ImageStorage\Security\SignatureStrategyInterface;
+use SixtyEightPublishers\ImageStorage\Bridge\Nette\DI\Config\StorageConfig;
+use SixtyEightPublishers\ImageStorage\ImageServer\ResponseFactoryInterface;
use SixtyEightPublishers\ImageStorage\LinkGenerator\LinkGeneratorInterface;
use SixtyEightPublishers\ImageStorage\Modifier\Facade\ModifierFacadeFactory;
+use SixtyEightPublishers\FileStorage\Bridge\Nette\DI\Config\FilesystemConfig;
use SixtyEightPublishers\ImageStorage\ImageServer\ExternalImageServerFactory;
use SixtyEightPublishers\ImageStorage\Modifier\Collection\ModifierCollection;
use SixtyEightPublishers\ImageStorage\ImageServer\ImageServerFactoryInterface;
use SixtyEightPublishers\ImageStorage\Modifier\Facade\ModifierFacadeInterface;
+use SixtyEightPublishers\ImageStorage\Bridge\Nette\ImageServer\ResponseFactory;
+use SixtyEightPublishers\ImageStorage\Bridge\Nette\Application\ImageServerRoute;
+use SixtyEightPublishers\ImageStorage\Bridge\Nette\DI\Config\ImageStorageConfig;
use SixtyEightPublishers\FileStorage\Bridge\Nette\DI\FileStorageConsoleExtension;
use SixtyEightPublishers\ImageStorage\Responsive\SrcSetGeneratorFactoryInterface;
-use SixtyEightPublishers\ImageStorage\ImageServer\Response\ResponseFactoryInterface;
+use SixtyEightPublishers\ImageStorage\Bridge\Nette\Application\ImageServerPresenter;
use SixtyEightPublishers\ImageStorage\Modifier\Facade\ModifierFacadeFactoryInterface;
use SixtyEightPublishers\ImageStorage\Modifier\Preset\PresetCollectionFactoryInterface;
-use SixtyEightPublishers\ImageStorage\Bridge\Nette\ImageServer\Response\ResponseFactory;
use SixtyEightPublishers\FileStorage\Bridge\Nette\DI\FileStorageDefinitionFactoryInterface;
use SixtyEightPublishers\ImageStorage\Modifier\Collection\ModifierCollectionFactoryInterface;
+use SixtyEightPublishers\FileStorage\Bridge\Nette\DI\Config\StorageConfig as FileStorageConfig;
use SixtyEightPublishers\ImageStorage\Bridge\Intervention\Image\ImageManager\ImageManagerFactory;
-use SixtyEightPublishers\FileStorage\Bridge\Console\Configurator\CleanCommandConfiguratorInterface;
use SixtyEightPublishers\ImageStorage\Bridge\Symfony\Console\Configurator\CleanCommandConfigurator;
use SixtyEightPublishers\ImageStorage\Bridge\Intervention\Image\ImageManager\ImageManagerFactoryInterface;
+use SixtyEightPublishers\FileStorage\Bridge\Symfony\Console\Configurator\CleanCommandConfiguratorInterface;
+use function assert;
+use function sprintf;
+use function is_array;
+use function array_diff;
+use function array_keys;
final class ImageStorageExtension extends CompilerExtension implements FileStorageDefinitionFactoryInterface
{
@@ -69,97 +82,103 @@ final class ImageStorageExtension extends CompilerExtension implements FileStora
public const IMAGE_SERVER_LOCAL = 'local';
public const IMAGE_SERVER_EXTERNAL = 'external';
- /** @var string[] */
- private $created = [];
+ /** @var array */
+ private array $managed = [];
+
+ /** @var array */
+ private array $routes = [];
+
+ private bool $imageServerPresenterRegistered = false;
- /**
- * {@inheritDoc}
- */
public function getConfigSchema(): Schema
{
return Expect::structure([
- 'driver' => Expect::anyOf(Statement::class, self::DRIVER_GD, self::DRIVER_IMAGICK, self::DRIVER_68PUBLISHERS_IMAGICK)->default(self::DRIVER_GD)->dynamic(),
- 'storages' => Expect::arrayOf(Expect::structure([
- 'source_filesystem' => Expect::structure([
- 'adapter' => Expect::anyOf(Expect::string(), Expect::type(Statement::class))->required()->before(static function ($factory) {
- return $factory instanceof Statement ? $factory : new Statement($factory);
- }),
- 'config' => Expect::array([
- FlysystemConfig::OPTION_VISIBILITY => Visibility::PRIVATE,
- FlysystemConfig::OPTION_DIRECTORY_VISIBILITY => Visibility::PRIVATE,
- ])->mergeDefaults(true),
- ]),
- 'server' => Expect::anyOf(self::IMAGE_SERVER_LOCAL, self::IMAGE_SERVER_EXTERNAL)->default(self::IMAGE_SERVER_LOCAL),
-
- 'no_image' => Expect::arrayOf('string|null')->default([
- 'default' => null,
- ])->mergeDefaults(true),
- 'no_image_patterns' => Expect::arrayOf('string'),
- 'presets' => Expect::arrayOf('array'),
-
- 'modifiers' => Expect::listOf('string|' . Statement::class)
- ->mergeDefaults(false)
- ->before(static function (array $items) {
- return array_map(static function ($item) {
- return $item instanceof Statement ? $item : new Statement($item);
- }, $items);
- })
- ->default([
- new Statement(Modifier\Original::class),
- new Statement(Modifier\Height::class),
- new Statement(Modifier\Width::class),
- new Statement(Modifier\AspectRatio::class),
- new Statement(Modifier\Fit::class),
- new Statement(Modifier\PixelDensity::class),
- new Statement(Modifier\Orientation::class),
- new Statement(Modifier\Quality::class),
- ]),
-
- 'applicators' => Expect::listOf('string|' . Statement::class)
- ->mergeDefaults(false)
- ->before(static function (array $items) {
- return array_map(static function ($item) {
- return $item instanceof Statement ? $item : new Statement($item);
- }, $items);
- })
- ->default([
- new Statement(Applicator\Orientation::class),
- new Statement(Applicator\Resize::class),
- new Statement(Applicator\Format::class), # must be last
- ]),
-
- 'validators' => Expect::listOf('string|' . Statement::class)
- ->mergeDefaults(false)
- ->before(static function (array $items) {
- return array_map(static function ($item) {
- return $item instanceof Statement ? $item : new Statement($item);
- }, $items);
- })
- ->default([
- new Statement(Validator\AllowedResolutionValidator::class),
- new Statement(Validator\AllowedPixelDensityValidator::class),
- new Statement(Validator\AllowedQualityValidator::class),
- ]),
- ])),
- ]);
+ 'driver' => Expect::anyOf(Statement::class, self::DRIVER_GD, self::DRIVER_IMAGICK, self::DRIVER_68PUBLISHERS_IMAGICK)
+ ->default(self::DRIVER_GD)
+ ->dynamic(),
+ 'storages' => Expect::arrayOf(
+ Expect::structure([
+ 'source_filesystem' => Expect::structure([
+ 'adapter' => Expect::anyOf(Expect::string(), Expect::type(Statement::class))
+ ->required()
+ ->before(static function ($factory) {
+ return $factory instanceof Statement ? $factory : new Statement($factory);
+ }),
+ 'config' => Expect::array([
+ FlysystemConfig::OPTION_VISIBILITY => Visibility::PRIVATE,
+ FlysystemConfig::OPTION_DIRECTORY_VISIBILITY => Visibility::PRIVATE,
+ ])->mergeDefaults(),
+ ])->castTo(FilesystemConfig::class),
+
+ 'server' => Expect::anyOf(self::IMAGE_SERVER_LOCAL, self::IMAGE_SERVER_EXTERNAL)
+ ->default(self::IMAGE_SERVER_LOCAL),
+ 'route' => Expect::bool(false),
+
+ 'no_image' => Expect::arrayOf('string', 'string')
+ ->default([]),
+ 'no_image_patterns' => Expect::arrayOf('string', 'string')
+ ->default([]),
+ 'presets' => Expect::arrayOf(
+ Expect::arrayOf(Expect::scalar(), 'string'),
+ 'string'
+ )->default([]),
+
+ 'modifiers' => Expect::listOf('string|' . Statement::class),
+ 'applicators' => Expect::listOf('string|' . Statement::class),
+ 'validators' => Expect::listOf('string|' . Statement::class),
+
+ ])->before(function (array $config): array {
+ $config['modifiers'] = $this->normalizeListOfStatementsWithDefaults(
+ is_array($config['modifiers'] ?? null) ? $config['modifiers'] : ['@default'],
+ [
+ new Statement(Modifier\Original::class),
+ new Statement(Modifier\Height::class),
+ new Statement(Modifier\Width::class),
+ new Statement(Modifier\AspectRatio::class),
+ new Statement(Modifier\Fit::class),
+ new Statement(Modifier\PixelDensity::class),
+ new Statement(Modifier\Orientation::class),
+ new Statement(Modifier\Quality::class),
+ ]
+ );
+
+ $config['applicators'] = $this->normalizeListOfStatementsWithDefaults(
+ is_array($config['applicators'] ?? null) ? $config['applicators'] : ['@default'],
+ [
+ new Statement(Applicator\Orientation::class),
+ new Statement(Applicator\Resize::class),
+ new Statement(Applicator\Format::class), # must be last
+ ]
+ );
+
+ $config['validators'] = $this->normalizeListOfStatementsWithDefaults(
+ is_array($config['validators'] ?? null) ? $config['validators'] : ['@default'],
+ [
+ new Statement(Validator\AllowedResolutionValidator::class),
+ new Statement(Validator\AllowedPixelDensityValidator::class),
+ new Statement(Validator\AllowedQualityValidator::class),
+ ]
+ );
+
+ return $config;
+ })->castTo(StorageConfig::class)
+ ),
+ ])->castTo(ImageStorageConfig::class);
}
- /**
- * {@inheritDoc}
- *
- * @throws \SixtyEightPublishers\FileStorage\Exception\RuntimeException
- */
public function loadConfiguration(): void
{
if (0 >= count($this->compiler->getExtensions(FileStorageExtension::class))) {
throw new RuntimeException(sprintf(
'The extension %s can be used only with %s.',
- static::class,
+ self::class,
FileStorageExtension::class
));
}
$builder = $this->getContainerBuilder();
+ $config = $this->getConfig();
+ assert($config instanceof ImageStorageConfig);
# Image manager
$builder->addDefinition($this->prefix('image_manager_factory'))
@@ -170,7 +189,7 @@ public function loadConfiguration(): void
$builder->addDefinition($this->prefix('image_manager'))
->setType(ImageManager::class)
->setFactory([$this->prefix('@image_manager_factory'), 'create'], [
- ['driver' => $this->config->driver],
+ ['driver' => $config->driver],
]);
# Modifier collection factory
@@ -192,8 +211,8 @@ public function loadConfiguration(): void
->setAutowired(false)
->setType(ModifierFacadeFactoryInterface::class)
->setFactory(ModifierFacadeFactory::class, [
- $this->prefix('@modifiers.preset_collection_factory'),
- $this->prefix('@modifiers.modifier_collection_factory'),
+ new Reference($this->prefix('modifiers.preset_collection_factory')),
+ new Reference($this->prefix('modifiers.modifier_collection_factory')),
]);
# Responsive - srcset generator factory
@@ -213,8 +232,8 @@ public function loadConfiguration(): void
->setType(StorageCleanerInterface::class)
->setFactory(StorageCleaner::class);
- # Console - extends clean command if the FileStorageConsoleExtension is registered
- if (0 < count($this->compiler->getExtensions(FileStorageConsoleExtension::class))) {
+ # Console - extends clean command configurator if the FileStorageConsoleExtension is registered
+ if (0 < \count($this->compiler->getExtensions(FileStorageConsoleExtension::class))) {
$builder->addDefinition($this->prefix('configurator.clean_command'))
->setType(CleanCommandConfiguratorInterface::class)
->setFactory(CleanCommandConfigurator::class)
@@ -223,56 +242,70 @@ public function loadConfiguration(): void
}
}
- /**
- * {@inheritDoc}
- *
- * @throws \SixtyEightPublishers\FileStorage\Exception\RuntimeException
- */
public function beforeCompile(): void
{
$builder = $this->getContainerBuilder();
- $diff = array_diff(array_keys($this->config->storages), $this->created);
+ $config = $this->getConfig();
+ assert($config instanceof ImageStorageConfig);
- if (0 < count($diff)) {
+ $diff = array_diff(array_keys($config->storages), $this->managed);
+
+ if (0 < \count($diff)) {
throw new RuntimeException(sprintf(
- 'Missing definition for storage with a name "%s" in configuration for the extension %s.',
+ 'Missing definition for a storage with the name "%s" in the configuration of the extension %s.',
array_shift($diff),
FileStorageExtension::class
));
}
- /** @var \Nette\DI\Definitions\ServiceDefinition $storageCleanerDecorator */
$storageCleanerDecorator = $builder->getDefinition($this->prefix('storage_cleaner'));
$defaultStorageCleaner = $builder->getDefinitionByType(StorageCleanerInterface::class);
+ assert($storageCleanerDecorator instanceof ServiceDefinition && $defaultStorageCleaner instanceof ServiceDefinition);
$storageCleanerDecorator->setArguments([$defaultStorageCleaner]);
- $storageCleanerDecorator->setAutowired(true);
+ $storageCleanerDecorator->setAutowired();
$defaultStorageCleaner->setAutowired(false);
+
+ if (empty($this->routes)) {
+ return;
+ }
+
+ $presenterFactory = $builder->getDefinitionByType(IPresenterFactory::class);
+ $router = $builder->getDefinition('router');
+ assert($presenterFactory instanceof ServiceDefinition && $router instanceof ServiceDefinition);
+
+ $presenterFactory->addSetup('setMapping', [
+ [
+ 'ImageStorage' => ['SixtyEightPublishers\\ImageStorage\\Bridge\\Nette\\Application', '*', '*Presenter'],
+ ],
+ ]);
+
+ foreach ($this->routes as $storageName => $basePath) {
+ $router->addSetup('prepend', [
+ 'router' => new Statement(ImageServerRoute::class, [
+ $storageName,
+ $basePath,
+ ]),
+ ]);
+ }
}
- /**
- * {@inheritDoc}
- */
- public function canCreateFileStorage(string $name, object $config): bool
+ public function canCreateFileStorage(string $name, FileStorageConfig $config): bool
{
- return isset($this->config->storages[$name]);
+ $extensionConfig = $this->getConfig();
+ assert($extensionConfig instanceof ImageStorageConfig);
+
+ return isset($extensionConfig->storages[$name]);
}
- /**
- * {@inheritDoc}
- */
- public function createFileStorage(string $name, object $config): Definition
+ public function createFileStorage(string $name, FileStorageConfig $config): ServiceDefinition
{
- if (!$this->canCreateFileStorage($name, $config)) {
- throw new RuntimeException(sprintf(
- 'Can\'t create image storage with names "%s".',
- $name
- ));
- }
-
$builder = $this->getContainerBuilder();
- $imageStorageConfig = $this->config->storages[$name];
- $this->created[] = $name;
+ $extensionConfig = $this->getConfig();
+ assert($extensionConfig instanceof ImageStorageConfig);
+
+ $this->managed[] = $name;
+ $imageStorageConfig = $extensionConfig->storages[$name];
$builder->addDefinition($this->prefix('filesystem.' . $name))
->setType(FilesystemOperator::class)
@@ -298,7 +331,7 @@ public function createFileStorage(string $name, object $config): Definition
$builder->addDefinition($this->prefix('modifier_facade.' . $name))
->setType(ModifierFacadeInterface::class)
->setFactory(new Statement([$this->prefix('@modifiers.modifier_facade_factory'), 'create'], [
- $this->prefix('@config.' . $name),
+ new Reference($this->prefix('config.' . $name)),
]))
->addSetup('setModifiers', [$imageStorageConfig->modifiers])
->addSetup('setPresets', [$imageStorageConfig->presets])
@@ -309,9 +342,9 @@ public function createFileStorage(string $name, object $config): Definition
$builder->addDefinition($this->prefix('resource_factory.' . $name))
->setType(ResourceFactoryInterface::class)
->setFactory(ResourceFactory::class, [
- $this->prefix('@filesystem.' . $name),
- $this->prefix('@image_manager'),
- $this->prefix('@modifier_facade.' . $name),
+ new Reference($this->prefix('filesystem.' . $name)),
+ new Reference($this->prefix('image_manager')),
+ new Reference($this->prefix('modifier_facade.' . $name)),
])
->setAutowired(false);
@@ -320,7 +353,7 @@ public function createFileStorage(string $name, object $config): Definition
$signatureStrategyDefinition = $builder->addDefinition($this->prefix('signature_strategy.' . $name))
->setType(SignatureStrategyInterface::class)
->setFactory(SignatureStrategy::class, [
- $this->prefix('@config.' . $name),
+ new Reference($this->prefix('config.' . $name)),
])
->setAutowired(false);
}
@@ -328,9 +361,9 @@ public function createFileStorage(string $name, object $config): Definition
$builder->addDefinition($this->prefix('link_generator.' . $name))
->setType(LinkGeneratorInterface::class)
->setFactory(LinkGenerator::class, [
- $this->prefix('@config.' . $name),
- $this->prefix('@modifier_facade.' . $name),
- $this->prefix('@responsive.srcset_generator_factory'),
+ new Reference($this->prefix('config.' . $name)),
+ new Reference($this->prefix('modifier_facade.' . $name)),
+ new Reference($this->prefix('responsive.srcset_generator_factory')),
$signatureStrategyDefinition ?? null,
])
->setAutowired(false);
@@ -338,29 +371,33 @@ public function createFileStorage(string $name, object $config): Definition
$builder->addDefinition($this->prefix('image_persister.' . $name))
->setType(ImagePersisterInterface::class)
->setFactory(ImagePersister::class, [
- $this->prefix('@filesystem.' . $name),
- $this->prefix('@config.' . $name),
- $this->prefix('@modifier_facade.' . $name),
+ new Reference($this->prefix('filesystem.' . $name)),
+ new Reference($this->prefix('config.' . $name)),
+ new Reference($this->prefix('modifier_facade.' . $name)),
])
->setAutowired(false);
$builder->addDefinition($this->prefix('info_factory.' . $name))
->setType(InfoFactoryInterface::class)
->setFactory(InfoFactory::class, [
- $this->prefix('@modifier_facade.' . $name),
- $this->prefix('@link_generator.' . $name),
+ new Reference($this->prefix('modifier_facade.' . $name)),
+ new Reference($this->prefix('link_generator.' . $name)),
$name,
])
->setAutowired(false);
- $defaultNoImage = $imageStorageConfig->no_image['default'];
- unset($imageStorageConfig->no_image['default']);
+ $noImages = $imageStorageConfig->no_image;
+ $defaultNoImage = $noImages['default'] ?? null;
+
+ if (null !== $defaultNoImage) {
+ unset($noImages['default']);
+ }
$builder->addDefinition($this->prefix('no_image_config.' . $name))
->setType(NoImageConfigInterface::class)
->setFactory(NoImageConfig::class, [
$defaultNoImage,
- $imageStorageConfig->no_image,
+ $noImages,
$imageStorageConfig->no_image_patterns,
])
->setAutowired(false);
@@ -368,8 +405,8 @@ public function createFileStorage(string $name, object $config): Definition
$builder->addDefinition($this->prefix('no_image_resolver.' . $name))
->setType(NoImageResolverInterface::class)
->setFactory(NoImageResolver::class, [
- $this->prefix('@info_factory.' . $name),
- $this->prefix('@no_image_config.' . $name),
+ new Reference($this->prefix('info_factory.' . $name)),
+ new Reference($this->prefix('no_image_config.' . $name)),
])
->setAutowired(false);
@@ -379,10 +416,32 @@ public function createFileStorage(string $name, object $config): Definition
switch ($imageStorageConfig->server) {
case self::IMAGE_SERVER_LOCAL:
- $imageServerDefinition->setFactory(LocalImageServerFactory::class, [$this->prefix('@image_server_response_factory')]);
+ if ($imageStorageConfig->route && '' === ($config->config[ConfigInterface::BASE_PATH] ?? '')) {
+ throw new RuntimeException(sprintf(
+ 'Unable to register a route for an image storage with the name "%s". Please set a configuration option "%s".',
+ $name,
+ ConfigInterface::BASE_PATH
+ ));
+ }
+
+ if ($imageStorageConfig->route) {
+ $this->registerImageServerPresenter();
+ $this->routes[$name] = $config->config[ConfigInterface::BASE_PATH];
+ }
+
+ $imageServerDefinition->setFactory(LocalImageServerFactory::class, [
+ new Reference($this->prefix('image_server_response_factory')),
+ ]);
break;
case self::IMAGE_SERVER_EXTERNAL:
+ if ($imageStorageConfig->route) {
+ throw new RuntimeException(sprintf(
+ 'Unable to register a route for an image storage with the name "%s" because a server is set as external.',
+ $name
+ ));
+ }
+
$imageServerDefinition->setFactory(ExternalImageServerFactory::class);
break;
@@ -392,14 +451,57 @@ public function createFileStorage(string $name, object $config): Definition
->setType(ImageStorageInterface::class)
->setFactory(ImageStorage::class, [
$name,
- $this->prefix('@config.' . $name),
- $this->prefix('@resource_factory.' . $name),
- $this->prefix('@link_generator.' . $name),
- $this->prefix('@image_persister.' . $name),
- $this->prefix('@no_image_resolver.' . $name),
- $this->prefix('@info_factory.' . $name),
+ new Reference($this->prefix('config.' . $name)),
+ new Reference($this->prefix('resource_factory.' . $name)),
+ new Reference($this->prefix('link_generator.' . $name)),
+ new Reference($this->prefix('image_persister.' . $name)),
+ new Reference($this->prefix('no_image_resolver.' . $name)),
+ new Reference($this->prefix('info_factory.' . $name)),
$imageServerDefinition,
])
->setAutowired(false);
}
+
+ /**
+ * @param array $items
+ * @param array $defaults
+ *
+ * @return array
+ */
+ private function normalizeListOfStatementsWithDefaults(array $items, array $defaults): array
+ {
+ $statements = [];
+ $defaultsMerged = false;
+
+ foreach ($items as $item) {
+ if (!$defaultsMerged && '@default' === $item) {
+ foreach ($defaults as $default) {
+ $statements[] = $default;
+ }
+
+ $defaultsMerged = true;
+
+ continue;
+ }
+
+ if (!$item instanceof Statement) {
+ $item = new Statement($item);
+ }
+
+ $statements[] = $item;
+ }
+
+ return $statements;
+ }
+
+ private function registerImageServerPresenter(): void
+ {
+ if ($this->imageServerPresenterRegistered) {
+ return;
+ }
+
+ $this->getContainerBuilder()
+ ->addDefinition($this->prefix('presenter.image_server'))
+ ->setType(ImageServerPresenter::class);
+ }
}
diff --git a/src/Bridge/Nette/DI/ImageStorageLambdaExtension.php b/src/Bridge/Nette/DI/ImageStorageLambdaExtension.php
index 2754c33..b4d97ea 100644
--- a/src/Bridge/Nette/DI/ImageStorageLambdaExtension.php
+++ b/src/Bridge/Nette/DI/ImageStorageLambdaExtension.php
@@ -8,127 +8,57 @@
use Nette\Schema\Schema;
use Nette\DI\CompilerExtension;
use Yosymfony\Toml\TomlBuilder;
-use Nette\DI\Helpers as DIHelpers;
-use Nette\DI\Definitions\Statement;
use League\Flysystem\AwsS3V3\AwsS3V3Adapter;
use SixtyEightPublishers\FileStorage\Exception\RuntimeException;
-use SixtyEightPublishers\ImageStorage\Bridge\ImageStorageLambda\Stack\Stack;
+use SixtyEightPublishers\ImageStorage\Bridge\ImageStorageLambda\LambdaConfig;
use SixtyEightPublishers\ImageStorage\Bridge\ImageStorageLambda\SamConfigGenerator;
-use SixtyEightPublishers\ImageStorage\Bridge\ImageStorageLambda\Builder\TomlConfigBuilder;
+use SixtyEightPublishers\ImageStorage\Bridge\Nette\DI\Config\ImageStorageLambdaConfig;
use SixtyEightPublishers\ImageStorage\Bridge\ImageStorageLambda\SamConfigGeneratorInterface;
use SixtyEightPublishers\ImageStorage\Bridge\Symfony\Console\Command\DumpLambdaConfigCommand;
-use SixtyEightPublishers\ImageStorage\Bridge\ImageStorageLambda\Builder\TomlConfigBuilderFactoryInterface;
+use function assert;
final class ImageStorageLambdaExtension extends CompilerExtension
{
- public const CAPABILITY_IAM = TomlConfigBuilder::CAPABILITY_IAM;
- public const CAPABILITY_NAMED_IAM = TomlConfigBuilder::CAPABILITY_NAMED_IAM;
-
- /**
- * {@inheritDoc}
- */
public function getConfigSchema(): Schema
{
- $stack = Expect::structure([
- 'stack_name' => Expect::string()->required()->dynamic(),
- 's3_bucket' => Expect::string()->required()->dynamic(),
- 'region' => Expect::string()->required()->dynamic(),
- 'version' => Expect::float(1.0)->dynamic(),
- 's3_prefix' => Expect::string()->nullable()->dynamic(), # an option "stack_name" is used by default
- 'confirm_changeset' => Expect::bool(false)->dynamic(),
- 'capabilities' => Expect::anyOf(self::CAPABILITY_IAM, self::CAPABILITY_NAMED_IAM)->default(self::CAPABILITY_IAM)->dynamic(),
-
- 'source_bucket_name' => Expect::string()->nullable()->dynamic(), # detected automatically from AwsS3V3Adapter by default
- 'cache_bucket_name' => Expect::string()->nullable()->dynamic(), # detected automatically from AwsS3V3Adapter by default
- ]);
-
- $stack->before(static function (array $stack) {
- if (empty($stack['s3_prefix'] ?? '') && !empty($stack['stack_name'] ?? '')) {
- $stack['s3_prefix'] = $stack['stack_name'];
- }
-
- return $stack;
- });
+ $appDir = $this->getContainerBuilder()->parameters['appDir'] ?? '';
return Expect::structure([
- 'output_dir' => Expect::string('%appDir%/config/image-storage-lambda')->dynamic()->before(function ($dir) {
- return is_string($dir) ? DIHelpers::expand($dir, $this->getContainerBuilder()->parameters) : $dir;
- }),
- 'stacks' => Expect::arrayOf($stack),
- ]);
+ 'output_dir' => Expect::string($appDir . '/config/image-storage-lambda')->dynamic(),
+ 'stacks' => Expect::arrayOf(LambdaConfig::createSchema(), 'string'),
+ ])->castTo(ImageStorageLambdaConfig::class);
}
- /**
- * {@inheritdoc}
- *
- * @throws \SixtyEightPublishers\FileStorage\Exception\RuntimeException
- */
public function loadConfiguration(): void
{
if (0 >= count($this->compiler->getExtensions(ImageStorageExtension::class))) {
throw new RuntimeException(sprintf(
'The extension %s can be used only with %s.',
- static::class,
+ self::class,
ImageStorageExtension::class
));
}
if (!class_exists(TomlBuilder::class)) {
- throw new RuntimeException('Please require a package yosymfony/toml in your project.');
+ throw new RuntimeException('Please require the package yosymfony/toml in your project.');
}
if (!class_exists(AwsS3V3Adapter::class)) {
- throw new RuntimeException('Please require a package league/flysystem-aws-s3-v3 in your project.');
+ throw new RuntimeException('Please require the package league/flysystem-aws-s3-v3 in your project.');
}
$builder = $this->getContainerBuilder();
-
- $builder->addFactoryDefinition($this->prefix('toml_config_builder_factory'))
- ->setAutowired(false)
- ->setImplement(TomlConfigBuilderFactoryInterface::class)
- ->getResultDefinition()
- ->setFactory(TomlConfigBuilder::class);
+ $config = $this->getConfig();
+ assert($config instanceof ImageStorageLambdaConfig);
$builder->addDefinition($this->prefix('sam_config_generator'))
->setType(SamConfigGeneratorInterface::class)
->setFactory(SamConfigGenerator::class, [
- $this->prefix('@toml_config_builder_factory'),
- $this->config->output_dir,
- $this->createStacks(),
+ $config->output_dir,
+ array_map(static fn (LambdaConfig $lambdaConfig): array => $lambdaConfig->toArray(), $config->stacks),
]);
$builder->addDefinition($this->prefix('command.dump_lambda_config'))
->setType(DumpLambdaConfigCommand::class);
}
-
- /**
- * @return array
- */
- private function createStacks(): array
- {
- $stacks = [];
-
- foreach ($this->config->stacks as $stackName => $stackConfig) {
- $stackConfig = (array) $stackConfig;
- $sourceBucketName = $stackConfig['source_bucket_name'] ?? null;
- $cacheBucketName = $stackConfig['cache_bucket_name'] ?? null;
-
- if (array_key_exists('source_bucket_name', $stackConfig)) {
- unset($stackConfig['source_bucket_name']);
- }
-
- if (array_key_exists('cache_bucket_name', $stackConfig)) {
- unset($stackConfig['cache_bucket_name']);
- }
-
- $stacks[] = new Statement(Stack::class, [
- $stackName,
- $stackConfig,
- $sourceBucketName,
- $cacheBucketName,
- ]);
- }
-
- return $stacks;
- }
}
diff --git a/src/Bridge/Nette/DI/ImageStorageLatteExtension.php b/src/Bridge/Nette/DI/ImageStorageLatteExtension.php
index 7ba5907..d520344 100644
--- a/src/Bridge/Nette/DI/ImageStorageLatteExtension.php
+++ b/src/Bridge/Nette/DI/ImageStorageLatteExtension.php
@@ -5,70 +5,41 @@
namespace SixtyEightPublishers\ImageStorage\Bridge\Nette\DI;
use Latte\Engine;
-use Nette\Schema\Expect;
-use Nette\Schema\Schema;
use Nette\DI\CompilerExtension;
-use Nette\PhpGenerator\PhpLiteral;
+use Nette\DI\Definitions\Reference;
+use Nette\DI\Definitions\Statement;
use Nette\DI\Definitions\FactoryDefinition;
use SixtyEightPublishers\FileStorage\Exception\RuntimeException;
use SixtyEightPublishers\FileStorage\FileStorageProviderInterface;
-use SixtyEightPublishers\ImageStorage\Bridge\Latte\ImageStorageFunctions;
+use SixtyEightPublishers\ImageStorage\Bridge\Latte\ImageStorageLatteExtension as LatteExtension;
+use function count;
+use function assert;
+use function sprintf;
final class ImageStorageLatteExtension extends CompilerExtension
{
- /**
- * {@inheritDoc}
- */
- public function getConfigSchema(): Schema
- {
- $functionNames = [];
-
- foreach (ImageStorageFunctions::DEFAULT_FUNCTION_NAMES as $functionId => $defaultFunctionName) {
- $functionNames[$functionId] = Expect::string($defaultFunctionName);
- }
-
- return Expect::structure([
- 'function_names' => Expect::structure($functionNames),
- # create_w_descriptor: w_descriptor
- # create_x_descriptor: x_descriptor
- # create_w_descriptor_from_range: w_descriptor_range
- # create_no_image: no_image
- ]);
- }
-
- /**
- * {@inheritDoc}
- *
- * @throws \SixtyEightPublishers\FileStorage\Exception\RuntimeException
- */
public function loadConfiguration(): void
{
if (0 >= count($this->compiler->getExtensions(ImageStorageExtension::class))) {
throw new RuntimeException(sprintf(
'The extension %s can be used only with %s.',
- static::class,
+ self::class,
ImageStorageExtension::class
));
}
}
- /**
- * {@inheritDoc}
- */
public function beforeCompile(): void
{
$builder = $this->getContainerBuilder();
$latteFactory = $builder->getDefinition($builder->getByType(Engine::class) ?? 'nette.latteFactory');
+ assert($latteFactory instanceof FactoryDefinition);
+ $resultDefinition = $latteFactory->getResultDefinition();
- if ($latteFactory instanceof FactoryDefinition) {
- $latteFactory = $latteFactory->getResultDefinition();
- }
-
- $latteFactory->addSetup('?::register(?, ?, ?)', [
- new PhpLiteral(ImageStorageFunctions::class),
- '@' . FileStorageProviderInterface::class,
- '@self',
- (array) $this->config->function_names,
+ $resultDefinition->addSetup('addExtension', [
+ new Statement(LatteExtension::class, [
+ new Reference(FileStorageProviderInterface::class),
+ ]),
]);
}
}
diff --git a/src/Bridge/Nette/ImageServer/Response/ErrorResponse.php b/src/Bridge/Nette/ImageServer/ErrorResponse.php
similarity index 60%
rename from src/Bridge/Nette/ImageServer/Response/ErrorResponse.php
rename to src/Bridge/Nette/ImageServer/ErrorResponse.php
index 17549b0..523a5f4 100644
--- a/src/Bridge/Nette/ImageServer/Response/ErrorResponse.php
+++ b/src/Bridge/Nette/ImageServer/ErrorResponse.php
@@ -2,36 +2,29 @@
declare(strict_types=1);
-namespace SixtyEightPublishers\ImageStorage\Bridge\Nette\ImageServer\Response;
+namespace SixtyEightPublishers\ImageStorage\Bridge\Nette\ImageServer;
+use JsonException;
use Nette\Http\IRequest;
use Nette\Http\IResponse;
use Nette\Application\IResponse as ApplicationResponse;
use SixtyEightPublishers\ImageStorage\Exception\ResponseException;
+use function json_encode;
final class ErrorResponse implements ApplicationResponse
{
- /** @var \SixtyEightPublishers\ImageStorage\Exception\ResponseException */
- private $exception;
-
- /**
- * @param \SixtyEightPublishers\ImageStorage\Exception\ResponseException $exception
- */
- public function __construct(ResponseException $exception)
- {
- $this->exception = $exception;
+ public function __construct(
+ private readonly ResponseException $exception,
+ ) {
}
- /**
- * @return \SixtyEightPublishers\ImageStorage\Exception\ResponseException
- */
public function getException(): ResponseException
{
return $this->exception;
}
/**
- * {@inheritdoc}
+ * @throws JsonException
*/
public function send(IRequest $httpRequest, IResponse $httpResponse): void
{
@@ -40,6 +33,6 @@ public function send(IRequest $httpRequest, IResponse $httpResponse): void
echo json_encode([
'code' => $this->exception->getHttpCode(),
'message' => $this->exception->getMessage(),
- ], JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);
+ ], JSON_THROW_ON_ERROR);
}
}
diff --git a/src/Bridge/Nette/ImageServer/Response/ImageResponse.php b/src/Bridge/Nette/ImageServer/ImageResponse.php
similarity index 69%
rename from src/Bridge/Nette/ImageServer/Response/ImageResponse.php
rename to src/Bridge/Nette/ImageServer/ImageResponse.php
index a0b6c2c..969669e 100644
--- a/src/Bridge/Nette/ImageServer/Response/ImageResponse.php
+++ b/src/Bridge/Nette/ImageServer/ImageResponse.php
@@ -2,7 +2,7 @@
declare(strict_types=1);
-namespace SixtyEightPublishers\ImageStorage\Bridge\Nette\ImageServer\Response;
+namespace SixtyEightPublishers\ImageStorage\Bridge\Nette\ImageServer;
use DateTime;
use Exception;
@@ -14,33 +14,22 @@
use League\Flysystem\FilesystemException;
use Nette\Application\IResponse as ApplicationResponse;
use SixtyEightPublishers\ImageStorage\Exception\ResponseException;
+use function ftell;
+use function fclose;
+use function rewind;
+use function sprintf;
+use function fpassthru;
final class ImageResponse implements ApplicationResponse
{
- /** @var \League\Flysystem\FilesystemReader */
- private $filesystemReader;
-
- /** @var string */
- private $filePath;
-
- /** @var int */
- private $maxAge;
-
- /**
- * @param \League\Flysystem\FilesystemReader $filesystemReader
- * @param string $filePath
- * @param int $maxAge
- */
- public function __construct(FilesystemReader $filesystemReader, string $filePath, int $maxAge)
- {
- $this->filesystemReader = $filesystemReader;
- $this->filePath = $filePath;
- $this->maxAge = $maxAge;
+ public function __construct(
+ private readonly FilesystemReader $filesystemReader,
+ private readonly string $filePath,
+ private readonly int $maxAge
+ ) {
}
/**
- * {@inheritDoc}
- *
* @throws Exception
*/
public function send(IRequest $httpRequest, IResponse $httpResponse): void
@@ -54,9 +43,9 @@ public function send(IRequest $httpRequest, IResponse $httpResponse): void
->setHeader('Cache-Control', sprintf('public, max-age=%s', $this->maxAge))
->setHeader('Expires', (new DateTime(sprintf('+%s seconds', $this->maxAge), new DateTimeZone('GMT')))->format('D, d M Y H:i:s') . ' GMT');
} catch (UnableToReadFile $e) {
- $errorResponse = new ErrorResponse(new ResponseException('Unable to read file.', IResponse::S404_NOT_FOUND, $e));
+ $errorResponse = new ErrorResponse(new ResponseException('Unable to read file.', IResponse::S404_NotFound, $e));
} catch (FilesystemException $e) {
- $errorResponse = new ErrorResponse(new ResponseException('Filesystem error.', IResponse::S500_INTERNAL_SERVER_ERROR, $e));
+ $errorResponse = new ErrorResponse(new ResponseException('Filesystem error. ' . $e->getMessage(), IResponse::S500_InternalServerError, $e));
}
if (isset($errorResponse)) {
diff --git a/src/Bridge/Nette/ImageServer/Request.php b/src/Bridge/Nette/ImageServer/Request.php
new file mode 100644
index 0000000..34a040b
--- /dev/null
+++ b/src/Bridge/Nette/ImageServer/Request.php
@@ -0,0 +1,31 @@
+request->getUrl()->getPath();
+ }
+
+ public function getQueryParameter(string $name): array|string|null
+ {
+ return $this->request->getUrl()->getQueryParameter($name);
+ }
+
+ public function getOriginalRequest(): IRequest
+ {
+ return $this->request;
+ }
+}
diff --git a/src/Bridge/Nette/ImageServer/Request/Request.php b/src/Bridge/Nette/ImageServer/Request/Request.php
deleted file mode 100644
index 8ce90a4..0000000
--- a/src/Bridge/Nette/ImageServer/Request/Request.php
+++ /dev/null
@@ -1,46 +0,0 @@
-request = $request;
- }
-
- /**
- * {@inheritDoc}
- */
- public function getUrlPath(): string
- {
- return $this->request->getUrl()->getPath();
- }
-
- /**
- * {@inheritDoc}
- */
- public function getQueryParameter(string $name)
- {
- return $this->request->getUrl()->getQueryParameter($name);
- }
-
- /**
- * @return \Nette\Http\IRequest
- */
- public function getOriginalRequest(): IRequest
- {
- return $this->request;
- }
-}
diff --git a/src/Bridge/Nette/ImageServer/Response/ResponseFactory.php b/src/Bridge/Nette/ImageServer/ResponseFactory.php
similarity index 58%
rename from src/Bridge/Nette/ImageServer/Response/ResponseFactory.php
rename to src/Bridge/Nette/ImageServer/ResponseFactory.php
index f56abce..04d8ec0 100644
--- a/src/Bridge/Nette/ImageServer/Response/ResponseFactory.php
+++ b/src/Bridge/Nette/ImageServer/ResponseFactory.php
@@ -2,29 +2,27 @@
declare(strict_types=1);
-namespace SixtyEightPublishers\ImageStorage\Bridge\Nette\ImageServer\Response;
+namespace SixtyEightPublishers\ImageStorage\Bridge\Nette\ImageServer;
use League\Flysystem\FilesystemReader;
use SixtyEightPublishers\ImageStorage\Config\Config;
-use Nette\Application\IResponse as ApplicationResponse;
use SixtyEightPublishers\FileStorage\Config\ConfigInterface;
use SixtyEightPublishers\ImageStorage\Exception\ResponseException;
-use SixtyEightPublishers\ImageStorage\ImageServer\Response\ResponseFactoryInterface;
+use SixtyEightPublishers\ImageStorage\ImageServer\ResponseFactoryInterface;
+use function assert;
+use function is_int;
final class ResponseFactory implements ResponseFactoryInterface
{
- /**
- * {@inheritDoc}
- */
- public function createImageResponse(FilesystemReader $reader, string $path, ConfigInterface $config): ApplicationResponse
+ public function createImageResponse(FilesystemReader $reader, string $path, ConfigInterface $config): ImageResponse
{
- return new ImageResponse($reader, $path, (int) $config[Config::CACHE_MAX_AGE]);
+ $maxAge = $config[Config::CACHE_MAX_AGE];
+ assert(is_int($maxAge));
+
+ return new ImageResponse($reader, $path, $maxAge);
}
- /**
- * {@inheritDoc}
- */
- public function createErrorResponse(ResponseException $e, ConfigInterface $config): ApplicationResponse
+ public function createErrorResponse(ResponseException $e, ConfigInterface $config): ErrorResponse
{
return new ErrorResponse($e);
}
diff --git a/src/Bridge/Nette/Presenter/AbstractImageServerPresenter.php b/src/Bridge/Nette/Presenter/AbstractImageServerPresenter.php
deleted file mode 100644
index 23f5bf9..0000000
--- a/src/Bridge/Nette/Presenter/AbstractImageServerPresenter.php
+++ /dev/null
@@ -1,67 +0,0 @@
-request = $request;
- $this->fileStorageProvider = $fileStorageProvider;
- }
-
- /**
- * {@inheritdoc}
- */
- public function run(ApplicationRequest $request): ApplicationResponse
- {
- $storage = $this->fileStorageProvider->get($this->storageName);
-
- if (!$storage instanceof ImageStorageInterface) {
- throw new InvalidStateException(sprintf(
- 'File storage "%s" must be implementor is an interface %s.',
- $storage->getName(),
- ImageStorageInterface::class
- ));
- }
-
- $response = $storage->getImageResponse(new Request($this->request));
-
- if ($response instanceof ErrorResponse) {
- Debugger::log($response->getException(), ILogger::ERROR);
- }
-
- return $response;
- }
-}
diff --git a/src/Bridge/Symfony/Console/Command/DumpLambdaConfigCommand.php b/src/Bridge/Symfony/Console/Command/DumpLambdaConfigCommand.php
index e3392e8..03131b8 100644
--- a/src/Bridge/Symfony/Console/Command/DumpLambdaConfigCommand.php
+++ b/src/Bridge/Symfony/Console/Command/DumpLambdaConfigCommand.php
@@ -11,55 +11,72 @@
use SixtyEightPublishers\FileStorage\FileStorageInterface;
use SixtyEightPublishers\ImageStorage\ImageStorageInterface;
use SixtyEightPublishers\FileStorage\FileStorageProviderInterface;
+use SixtyEightPublishers\ImageStorage\Exception\InvalidArgumentException;
use SixtyEightPublishers\ImageStorage\Bridge\ImageStorageLambda\SamConfigGeneratorInterface;
+use function assert;
+use function sprintf;
+use function is_string;
+use function array_filter;
+use function iterator_to_array;
final class DumpLambdaConfigCommand extends Command
{
- /** @var \SixtyEightPublishers\ImageStorage\Bridge\ImageStorageLambda\SamConfigGeneratorInterface */
- private $samConfigGenerator;
+ protected static $defaultName = 'image-storage:lambda:dump-config';
- /** @var \SixtyEightPublishers\FileStorage\FileStorageProviderInterface */
- private $fileStorageProvider;
-
- /**
- * @param \SixtyEightPublishers\ImageStorage\Bridge\ImageStorageLambda\SamConfigGeneratorInterface $samConfigGenerator
- * @param \SixtyEightPublishers\FileStorage\FileStorageProviderInterface $fileStorageProvider
- */
- public function __construct(SamConfigGeneratorInterface $samConfigGenerator, FileStorageProviderInterface $fileStorageProvider)
- {
+ public function __construct(
+ private readonly SamConfigGeneratorInterface $samConfigGenerator,
+ private readonly FileStorageProviderInterface $fileStorageProvider,
+ ) {
parent::__construct();
-
- $this->samConfigGenerator = $samConfigGenerator;
- $this->fileStorageProvider = $fileStorageProvider;
}
- /**
- * {@inheritdoc}
- */
protected function configure(): void
{
- $this->setName('image-storage:lambda:dump-config')
- ->setDescription('Dumps AWS SAM configuration files for defined storages')
- ->addArgument('storage', InputArgument::OPTIONAL, 'Generate config for specific storage only.', null);
+ $this->setDescription('Dumps AWS SAM configuration files for defined storages')
+ ->addArgument('storage', InputArgument::OPTIONAL, 'Generate config for specific storage only.');
}
- /**
- * {@inheritdoc}
- */
public function execute(InputInterface $input, OutputInterface $output): int
{
$storageName = $input->getArgument('storage');
- $storages = null !== $storageName ? [$this->fileStorageProvider->get($storageName)] : array_filter(iterator_to_array($this->fileStorageProvider), function (FileStorageInterface $fileStorage) {
- return $fileStorage instanceof ImageStorageInterface && $this->samConfigGenerator->hasStackForStorage($fileStorage);
- });
+ $storages = is_string($storageName)
+ ? [$this->fileStorageProvider->get($storageName)]
+ : array_filter(
+ iterator_to_array($this->fileStorageProvider),
+ fn (FileStorageInterface $fileStorage): bool => $fileStorage instanceof ImageStorageInterface && $this->samConfigGenerator->canGenerate($fileStorage)
+ );
foreach ($storages as $storage) {
- $filename = $this->samConfigGenerator->generateForStorage($storage);
+ assert($storage instanceof FileStorageInterface);
+
+ if (!$storage instanceof ImageStorageInterface) {
+ throw new InvalidArgumentException(sprintf(
+ 'Storage "%s" is not an instance of %s.',
+ $storage->getName(),
+ ImageStorageInterface::class
+ ));
+ }
+ }
+
+ foreach ($storages as $storage) {
+ assert($storage instanceof ImageStorageInterface);
+
+ if (!$this->samConfigGenerator->canGenerate($storage)) {
+ throw new InvalidArgumentException(sprintf(
+ 'Lambda config for storage "%s" can not be generated.',
+ $storage->getName()
+ ));
+ }
+
+ $filename = $this->samConfigGenerator->generate($storage);
- $output->writeln('Successfully generated file ' . $filename);
+ $output->writeln(sprintf(
+ 'Successfully generated file %s',
+ $filename
+ ));
}
- return 0;
+ return Command::SUCCESS;
}
}
diff --git a/src/Config/Config.php b/src/Config/Config.php
index cdbb17c..a7f0228 100644
--- a/src/Config/Config.php
+++ b/src/Config/Config.php
@@ -19,7 +19,6 @@ final class Config extends FileStorageConfig
public const ENCODE_QUALITY = 'encode_quality';
public const CACHE_MAX_AGE = 'cache_max_age';
- /** @var array */
protected array $config = [
self::BASE_PATH => '',
self::HOST => null,
diff --git a/src/FileInfo.php b/src/FileInfo.php
index 091da9e..bd90668 100644
--- a/src/FileInfo.php
+++ b/src/FileInfo.php
@@ -7,16 +7,13 @@
use SixtyEightPublishers\FileStorage\PathInfoInterface;
use SixtyEightPublishers\FileStorage\FileInfo as BaseFileInfo;
use SixtyEightPublishers\ImageStorage\Exception\InvalidStateException;
-use SixtyEightPublishers\FileStorage\LinkGenerator\LinkGeneratorInterface;
use SixtyEightPublishers\ImageStorage\Responsive\Descriptor\DescriptorInterface;
use SixtyEightPublishers\ImageStorage\PathInfoInterface as ImagePathInfoInterface;
use SixtyEightPublishers\ImageStorage\LinkGenerator\LinkGeneratorInterface as ImageLinkGeneratorInterface;
+use function assert;
final class FileInfo extends BaseFileInfo implements FileInfoInterface
{
- /** @var ImageLinkGeneratorInterface */
- protected readonly LinkGeneratorInterface $linkGenerator;
-
public function __construct(ImageLinkGeneratorInterface $linkGenerator, PathInfoInterface $pathInfo, string $imageStorageName)
{
parent::__construct($linkGenerator, $pathInfo, $imageStorageName);
@@ -24,6 +21,8 @@ public function __construct(ImageLinkGeneratorInterface $linkGenerator, PathInfo
public function srcSet(DescriptorInterface $descriptor): string
{
+ assert($this->linkGenerator instanceof ImageLinkGeneratorInterface);
+
return $this->linkGenerator->srcSet($this, $descriptor);
}
diff --git a/src/Helper/SupportedType.php b/src/Helper/SupportedType.php
index e23b78e..f600bdd 100644
--- a/src/Helper/SupportedType.php
+++ b/src/Helper/SupportedType.php
@@ -94,13 +94,13 @@ public static function getExtensionByType(string $type): string
));
}
- return array_search($type, self::$supportedTypes, true);
+ return (string) array_search($type, self::$supportedTypes, true);
}
/**
* file extensions as keys and MimeTypes as values
*
- * @param array $types
+ * @param array $types
*
* @return void
*/
diff --git a/src/ImageServer/ImageServerInterface.php b/src/ImageServer/ImageServerInterface.php
index c609046..52cb922 100644
--- a/src/ImageServer/ImageServerInterface.php
+++ b/src/ImageServer/ImageServerInterface.php
@@ -4,8 +4,6 @@
namespace SixtyEightPublishers\ImageStorage\ImageServer;
-use SixtyEightPublishers\ImageStorage\ImageServer\Request\RequestInterface;
-
interface ImageServerInterface
{
public function getImageResponse(RequestInterface $request): object;
diff --git a/src/ImageServer/LocalImageServer.php b/src/ImageServer/LocalImageServer.php
index e022feb..7b60a1f 100644
--- a/src/ImageServer/LocalImageServer.php
+++ b/src/ImageServer/LocalImageServer.php
@@ -14,8 +14,6 @@
use SixtyEightPublishers\FileStorage\Exception\FileNotFoundException;
use SixtyEightPublishers\ImageStorage\Exception\InvalidArgumentException;
use SixtyEightPublishers\ImageStorage\Persistence\ImagePersisterInterface;
-use SixtyEightPublishers\ImageStorage\ImageServer\Request\RequestInterface;
-use SixtyEightPublishers\ImageStorage\ImageServer\Response\ResponseFactoryInterface;
use function count;
use function ltrim;
use function assert;
@@ -52,6 +50,7 @@ public function getImageResponse(RequestInterface $request): object
/**
* @throws \SixtyEightPublishers\FileStorage\Exception\FileNotFoundException
* @throws \SixtyEightPublishers\FileStorage\Exception\FilesystemException
+ * @throws \SixtyEightPublishers\ImageStorage\Exception\SignatureException
*/
private function processRequest(RequestInterface $request): object
{
@@ -151,6 +150,7 @@ private function validateSignature(RequestInterface $request, string $path): voi
assert(is_string($signatureParameterName));
$token = $request->getQueryParameter($signatureParameterName) ?? '';
+ assert(is_string($token));
if (empty($token)) {
throw new SignatureException('Missing signature in request.');
diff --git a/src/ImageServer/LocalImageServerFactory.php b/src/ImageServer/LocalImageServerFactory.php
index f04873c..8a2ace9 100644
--- a/src/ImageServer/LocalImageServerFactory.php
+++ b/src/ImageServer/LocalImageServerFactory.php
@@ -5,7 +5,6 @@
namespace SixtyEightPublishers\ImageStorage\ImageServer;
use SixtyEightPublishers\ImageStorage\ImageStorageInterface;
-use SixtyEightPublishers\ImageStorage\ImageServer\Response\ResponseFactoryInterface;
final class LocalImageServerFactory implements ImageServerFactoryInterface
{
diff --git a/src/ImageServer/Request/RequestInterface.php b/src/ImageServer/RequestInterface.php
similarity index 65%
rename from src/ImageServer/Request/RequestInterface.php
rename to src/ImageServer/RequestInterface.php
index 3a14ac9..b44de9c 100644
--- a/src/ImageServer/Request/RequestInterface.php
+++ b/src/ImageServer/RequestInterface.php
@@ -2,12 +2,15 @@
declare(strict_types=1);
-namespace SixtyEightPublishers\ImageStorage\ImageServer\Request;
+namespace SixtyEightPublishers\ImageStorage\ImageServer;
interface RequestInterface
{
public function getUrlPath(): string;
+ /**
+ * @return array|string|null
+ */
public function getQueryParameter(string $name): array|string|null;
public function getOriginalRequest(): object;
diff --git a/src/ImageServer/Response/ResponseFactoryInterface.php b/src/ImageServer/ResponseFactoryInterface.php
similarity index 87%
rename from src/ImageServer/Response/ResponseFactoryInterface.php
rename to src/ImageServer/ResponseFactoryInterface.php
index 94ee8c9..88b1e55 100644
--- a/src/ImageServer/Response/ResponseFactoryInterface.php
+++ b/src/ImageServer/ResponseFactoryInterface.php
@@ -2,7 +2,7 @@
declare(strict_types=1);
-namespace SixtyEightPublishers\ImageStorage\ImageServer\Response;
+namespace SixtyEightPublishers\ImageStorage\ImageServer;
use League\Flysystem\FilesystemReader;
use SixtyEightPublishers\FileStorage\Config\ConfigInterface;
diff --git a/src/ImageStorage.php b/src/ImageStorage.php
index 523333d..b36837c 100644
--- a/src/ImageStorage.php
+++ b/src/ImageStorage.php
@@ -8,25 +8,22 @@
use SixtyEightPublishers\FileStorage\PathInfoInterface;
use SixtyEightPublishers\FileStorage\Config\ConfigInterface;
use SixtyEightPublishers\ImageStorage\Info\InfoFactoryInterface;
+use SixtyEightPublishers\ImageStorage\ImageServer\RequestInterface;
use SixtyEightPublishers\ImageStorage\Config\NoImageConfigInterface;
use SixtyEightPublishers\FileStorage\Resource\ResourceFactoryInterface;
use SixtyEightPublishers\ImageStorage\ImageServer\ImageServerInterface;
use SixtyEightPublishers\ImageStorage\NoImage\NoImageResolverInterface;
-use SixtyEightPublishers\FileStorage\LinkGenerator\LinkGeneratorInterface;
use SixtyEightPublishers\ImageStorage\Persistence\ImagePersisterInterface;
use SixtyEightPublishers\ImageStorage\Security\SignatureStrategyInterface;
-use SixtyEightPublishers\ImageStorage\ImageServer\Request\RequestInterface;
use SixtyEightPublishers\ImageStorage\ImageServer\ImageServerFactoryInterface;
use SixtyEightPublishers\ImageStorage\Responsive\Descriptor\DescriptorInterface;
use SixtyEightPublishers\ImageStorage\FileInfoInterface as ImageFileInfoInterface;
use SixtyEightPublishers\ImageStorage\PathInfoInterface as ImagePathInfoInterface;
use SixtyEightPublishers\ImageStorage\LinkGenerator\LinkGeneratorInterface as ImageLinkGeneratorInterface;
+use function assert;
final class ImageStorage extends FileStorage implements ImageStorageInterface
{
- /** @var ImageLinkGeneratorInterface */
- protected readonly LinkGeneratorInterface $linkGenerator;
-
private ?ImageServerInterface $imageServer = null;
public function __construct(
@@ -94,11 +91,15 @@ public function resolveNoImage(string $path): ImagePathInfoInterface
public function srcSet(ImagePathInfoInterface $info, DescriptorInterface $descriptor): string
{
+ assert($this->linkGenerator instanceof ImageLinkGeneratorInterface);
+
return $this->linkGenerator->srcSet($info, $descriptor);
}
public function getSignatureStrategy(): ?SignatureStrategyInterface
{
+ assert($this->linkGenerator instanceof ImageLinkGeneratorInterface);
+
return $this->linkGenerator->getSignatureStrategy();
}
}
diff --git a/src/Modifier/AbstractModifier.php b/src/Modifier/AbstractModifier.php
index 44bd7be..60679f2 100644
--- a/src/Modifier/AbstractModifier.php
+++ b/src/Modifier/AbstractModifier.php
@@ -32,6 +32,6 @@ public function getName(): string
public function getAlias(): string
{
- return $this->alias;
+ return (string) $this->alias;
}
}
diff --git a/src/Modifier/Codec/Codec.php b/src/Modifier/Codec/Codec.php
index b2785f1..08c3be4 100644
--- a/src/Modifier/Codec/Codec.php
+++ b/src/Modifier/Codec/Codec.php
@@ -90,6 +90,9 @@ public function decode(ValueInterface $value): array
$separator = $this->config[Config::MODIFIER_SEPARATOR];
assert(\is_string($assigner) && \is_string($separator));
+ $assigner = empty($assigner) ? ':' : $assigner;
+ $separator = empty($separator) ? ',' : $separator;
+
foreach (explode($separator, $path) as $modifier) {
$modifier = explode($assigner, $modifier);
$count = count($modifier);
diff --git a/src/Modifier/Collection/ModifierCollectionInterface.php b/src/Modifier/Collection/ModifierCollectionInterface.php
index d848bba..3a09f7f 100644
--- a/src/Modifier/Collection/ModifierCollectionInterface.php
+++ b/src/Modifier/Collection/ModifierCollectionInterface.php
@@ -33,7 +33,7 @@ public function getByName(string $name): ModifierInterface;
public function getByAlias(string $alias): ModifierInterface;
/**
- * @param array $parameters
+ * @param array $parameters
*/
public function parseValues(array $parameters): ModifierValues;
}
diff --git a/src/Resource/ResourceFactory.php b/src/Resource/ResourceFactory.php
index 3baec51..66ce30e 100644
--- a/src/Resource/ResourceFactory.php
+++ b/src/Resource/ResourceFactory.php
@@ -59,7 +59,7 @@ public function createResource(PathInfoInterface $pathInfo): ResourceInterface
throw new FilesystemException($e->getMessage(), 0, $e);
}
- $tmpFilename = tempnam(sys_get_temp_dir(), '68Publishers_ImageStorage');
+ $tmpFilename = (string) tempnam(sys_get_temp_dir(), '68Publishers_ImageStorage');
if (false === file_put_contents($tmpFilename, $source)) {
throw new FilesystemException(sprintf(
diff --git a/src/Responsive/SrcSetGeneratorFactoryInterface.php b/src/Responsive/SrcSetGeneratorFactoryInterface.php
index 49625c6..24270ce 100644
--- a/src/Responsive/SrcSetGeneratorFactoryInterface.php
+++ b/src/Responsive/SrcSetGeneratorFactoryInterface.php
@@ -4,7 +4,7 @@
namespace SixtyEightPublishers\ImageStorage\Responsive;
-use SixtyEightPublishers\FileStorage\LinkGenerator\LinkGeneratorInterface;
+use SixtyEightPublishers\ImageStorage\LinkGenerator\LinkGeneratorInterface;
use SixtyEightPublishers\ImageStorage\Modifier\Facade\ModifierFacadeInterface;
interface SrcSetGeneratorFactoryInterface
diff --git a/tests/Bridge/ImageStorageLambda/LambdaConfigTest.phpt b/tests/Bridge/ImageStorageLambda/LambdaConfigTest.phpt
new file mode 100644
index 0000000..302b042
--- /dev/null
+++ b/tests/Bridge/ImageStorageLambda/LambdaConfigTest.phpt
@@ -0,0 +1,160 @@
+ 'test_bucket',
+ 'region' => 'west',
+ ]);
+
+ $this->assertConfig(
+ config: $config,
+ s3Bucket: 'test_bucket',
+ region: 'west'
+ );
+ }
+
+ public function testConfigWithRequiredValuesOnlyShouldBeCreatedFromStdClass(): void
+ {
+ $config = LambdaConfig::fromValues((object) [
+ 's3_bucket' => 'test_bucket',
+ 'region' => 'west',
+ ]);
+
+ $this->assertConfig(
+ config: $config,
+ s3Bucket: 'test_bucket',
+ region: 'west'
+ );
+ }
+
+ public function testConfigWithAllValuesShouldBeCreated(): void
+ {
+ $config = LambdaConfig::fromValues([
+ 'stack_name' => 'test_stack',
+ 'version' => 2.5,
+ 's3_bucket' => 'test_bucket',
+ 's3_prefix' => 'test_prefix',
+ 'region' => 'west',
+ 'confirm_changeset' => true,
+ 'capabilities' => LambdaConfig::CAPABILITY_NAMED_IAM,
+ 'parameter_overrides' => new ParameterOverrides(['TEST_KEY' => 'TEST_VALUE']),
+ 'source_bucket_name' => 'source',
+ 'cache_bucket_name' => 'cache',
+ ]);
+
+ $this->assertConfig(
+ config: $config,
+ s3Bucket: 'test_bucket',
+ region: 'west',
+ stackName: 'test_stack',
+ version: 2.5,
+ s3Prefix: 'test_prefix',
+ confirmChangeset: true,
+ capabilities: LambdaConfig::CAPABILITY_NAMED_IAM,
+ parameterOverrides: new ParameterOverrides(['TEST_KEY' => 'TEST_VALUE']),
+ sourceBucketName: 'source',
+ cacheBucketName: 'cache',
+ );
+ }
+
+ public function testS3PrefixShouldBeEqualToStackName(): void
+ {
+ $config = LambdaConfig::fromValues([
+ 'stack_name' => 'test_stack',
+ 's3_bucket' => 'test_bucket',
+ 'region' => 'west',
+ ]);
+
+ $this->assertConfig(
+ config: $config,
+ s3Bucket: 'test_bucket',
+ region: 'west',
+ stackName: 'test_stack',
+ s3Prefix: 'test_stack',
+ );
+ }
+
+ public function testParameterOverridesOptionShouldAcceptArray(): void
+ {
+ $config = LambdaConfig::fromValues([
+ 's3_bucket' => 'test_bucket',
+ 'region' => 'west',
+ 'parameter_overrides' => [
+ 'KEY_A' => 'VALUE_A',
+ 'KEY_B' => [
+ 'VALUE_B_1',
+ 'VALUE_B_2',
+ ],
+ 'KEY_C' => 15,
+ ],
+ ]);
+
+ $this->assertConfig(
+ config: $config,
+ s3Bucket: 'test_bucket',
+ region: 'west',
+ parameterOverrides: new ParameterOverrides([
+ 'KEY_A' => 'VALUE_A',
+ 'KEY_B' => [
+ 'VALUE_B_1',
+ 'VALUE_B_2',
+ ],
+ 'KEY_C' => 15,
+ ])
+ );
+ }
+
+ public function assertConfig(
+ LambdaConfig $config,
+ string $s3Bucket,
+ string $region,
+ ?string $stackName = null,
+ float $version = 1.0,
+ ?string $s3Prefix = null,
+ bool $confirmChangeset = false,
+ string $capabilities = LambdaConfig::CAPABILITY_IAM,
+ ?ParameterOverrides $parameterOverrides = new ParameterOverrides([]),
+ ?string $sourceBucketName = null,
+ ?string $cacheBucketName = null,
+ ): void {
+ Assert::same($s3Bucket, $config->s3_bucket);
+ Assert::same($region, $config->region);
+ Assert::same($stackName, $config->stack_name);
+ Assert::same($version, $config->version);
+ Assert::same($s3Prefix, $config->s3_prefix);
+ Assert::same($confirmChangeset, $config->confirm_changeset);
+ Assert::same($capabilities, $config->capabilities);
+ Assert::same($parameterOverrides->parameters, $config->parameter_overrides->parameters);
+ Assert::same($sourceBucketName, $config->source_bucket_name);
+ Assert::same($cacheBucketName, $config->cache_bucket_name);
+
+ Assert::same([
+ 'stack_name' => $stackName,
+ 'version' => $version,
+ 's3_bucket' => $s3Bucket,
+ 's3_prefix' => $s3Prefix,
+ 'region' => $region,
+ 'confirm_changeset' => $confirmChangeset,
+ 'capabilities' => $capabilities,
+ 'parameter_overrides' => $parameterOverrides->parameters,
+ 'source_bucket_name' => $sourceBucketName,
+ 'cache_bucket_name' => $cacheBucketName,
+ ], $config->toArray());
+ }
+}
+
+(new LambdaConfigTest())->run();
diff --git a/tests/Bridge/ImageStorageLambda/ParameterOverridesTest.phpt b/tests/Bridge/ImageStorageLambda/ParameterOverridesTest.phpt
new file mode 100644
index 0000000..20abec3
--- /dev/null
+++ b/tests/Bridge/ImageStorageLambda/ParameterOverridesTest.phpt
@@ -0,0 +1,75 @@
+ 'VALUE_A',
+ 'KEY_B' => [
+ 'VALUE_B_1',
+ 'VALUE_B_2',
+ ],
+ 'KEY_C' => 15,
+ ]);
+
+ $extendedParameters = $parameters->withMissingParameters([
+ 'KEY_B' => [
+ 'VALUE_B_3',
+ ],
+ 'KEY_D' => true,
+ ]);
+
+ Assert::notSame($parameters, $extendedParameters);
+ Assert::same([
+ 'KEY_A' => 'VALUE_A',
+ 'KEY_B' => [
+ 'VALUE_B_1',
+ 'VALUE_B_2',
+ ],
+ 'KEY_C' => 15,
+ ], $parameters->parameters);
+ Assert::same([
+ 'KEY_A' => 'VALUE_A',
+ 'KEY_B' => [
+ 'VALUE_B_1',
+ 'VALUE_B_2',
+ ],
+ 'KEY_C' => 15,
+ 'KEY_D' => true,
+ ], $extendedParameters->parameters);
+ }
+
+ public function testEmptyParametersShouldBeConvertedIntoEmptyString(): void
+ {
+ $parameters = new ParameterOverrides([]);
+
+ Assert::same('', (string) $parameters);
+ }
+
+ public function testParametersShouldBeConvertedIntoString(): void
+ {
+ $parameters = new ParameterOverrides([
+ 'KEY_A' => 'VALUE_A',
+ 'KEY_B' => [
+ 'VALUE_B_1',
+ 'VALUE_B_2',
+ ],
+ 'KEY_C' => 15,
+ ]);
+
+ Assert::same('KEY_A="VALUE_A" KEY_B="VALUE_B_1,VALUE_B_2" KEY_C="15"', (string) $parameters);
+ }
+}
+
+(new ParameterOverridesTest())->run();
diff --git a/tests/Bridge/ImageStorageLambda/SamConfigGeneratorTest.phpt b/tests/Bridge/ImageStorageLambda/SamConfigGeneratorTest.phpt
new file mode 100644
index 0000000..911e6b0
--- /dev/null
+++ b/tests/Bridge/ImageStorageLambda/SamConfigGeneratorTest.phpt
@@ -0,0 +1,272 @@
+outputDir, [
+ 'a' => [],
+ ]);
+
+ $imageStorageA = Mockery::mock(ImageStorageInterface::class);
+ $imageStorageB = Mockery::mock(ImageStorageInterface::class);
+
+ $imageStorageA->shouldReceive('getName')
+ ->withNoArgs()
+ ->andReturn('a');
+
+ $imageStorageB->shouldReceive('getName')
+ ->withNoArgs()
+ ->andReturn('b');
+
+ Assert::true($generator->canGenerate($imageStorageA));
+ Assert::false($generator->canGenerate($imageStorageB));
+ }
+
+ public function testExceptionShouldBeThrownIfConfigIsNotDefined(): void
+ {
+ $generator = new SamConfigGenerator($this->outputDir, [
+ 'a' => [],
+ ]);
+
+ $imageStorage = Mockery::mock(ImageStorageInterface::class);
+
+ $imageStorage->shouldReceive('getName')
+ ->withNoArgs()
+ ->andReturn('b');
+
+ Assert::exception(
+ static fn () => $generator->generate($imageStorage),
+ InvalidArgumentException::class,
+ 'Missing config with the name "b".'
+ );
+ }
+
+ public function testExceptionShouldBeThrownIfFilesystemIsNotInstanceOfAdapterProviderInterface(): void
+ {
+ $generator = new SamConfigGenerator($this->outputDir, [
+ 'a' => [
+ 's3_bucket' => 'test_bucket',
+ 'region' => 'west',
+ ],
+ ]);
+
+ $imageStorage = Mockery::mock(ImageStorageInterface::class);
+
+ $imageStorage->shouldReceive('getName')
+ ->withNoArgs()
+ ->andReturn('a');
+
+ $imageStorage->shouldReceive('getFilesystem')
+ ->once()
+ ->withNoArgs()
+ ->andReturn(Mockery::mock(FilesystemOperator::class));
+
+ Assert::exception(
+ static fn () => $generator->generate($imageStorage),
+ InvalidStateException::class,
+ 'Can\'t detect bucket names from a filesystem because the filesystem must be an implementor of SixtyEightPublishers\ImageStorage\Filesystem\AdapterProviderInterface.'
+ );
+ }
+
+ public function testExceptionShouldBeThrownIfFilesystemAdapterIsNotInstanceOfAwsS3V3Adapter(): void
+ {
+ $generator = new SamConfigGenerator($this->outputDir, [
+ 'a' => [
+ 's3_bucket' => 'test_bucket',
+ 'region' => 'west',
+ ],
+ ]);
+
+ $imageStorage = Mockery::mock(ImageStorageInterface::class);
+
+ $imageStorage->shouldReceive('getName')
+ ->withNoArgs()
+ ->andReturn('a');
+
+ $imageStorage->shouldReceive('getFilesystem')
+ ->once()
+ ->withNoArgs()
+ ->andReturn($this->createFilesystemInMemoryMountManager());
+
+ Assert::exception(
+ static fn () => $generator->generate($imageStorage),
+ InvalidStateException::class,
+ 'Adapter must be an instance of League\Flysystem\AwsS3V3\AwsS3V3Adapter.'
+ );
+ }
+
+ public function testConfigShouldBeGeneratedWithMinimalConfiguration(): void
+ {
+ $generator = new SamConfigGenerator($this->outputDir, [
+ 'minimal' => [
+ 's3_bucket' => 'test_bucket',
+ 'region' => 'west',
+ ],
+ ]);
+
+ $imageStorage = Mockery::mock(ImageStorageInterface::class);
+
+ $imageStorage->shouldReceive('getName')
+ ->withNoArgs()
+ ->andReturn('minimal');
+
+ $imageStorage->shouldReceive('getFilesystem')
+ ->once()
+ ->withNoArgs()
+ ->andReturn($this->createFilesystemS3MountManager());
+
+ $imageStorage->shouldReceive('getConfig')
+ ->once()
+ ->withNoArgs()
+ ->andReturn(new Config([]));
+
+ $imageStorage->shouldReceive('getNoImageConfig')
+ ->once()
+ ->withNoArgs()
+ ->andReturn(new NoImageConfig(null, [], []));
+
+ $filename = $this->outputDir . '/minimal/samconfig.toml';
+
+ try {
+ $generator->generate($imageStorage);
+
+ Assert::true(file_exists($filename));
+ Assert::same(file_get_contents(__DIR__ . '/config.minimalConfiguration.toml'), file_get_contents($filename));
+ } finally {
+ @unlink($filename);
+ }
+ }
+
+ public function testConfigShouldBeGeneratedWithFullConfiguration(): void
+ {
+ $generator = new SamConfigGenerator($this->outputDir, [
+ 'full' => [
+ 'stack_name' => 'test_stack',
+ 'version' => 2.5,
+ 's3_bucket' => 'test_bucket',
+ 's3_prefix' => 'test_prefix',
+ 'region' => 'west',
+ 'confirm_changeset' => true,
+ 'capabilities' => LambdaConfig::CAPABILITY_NAMED_IAM,
+ 'parameter_overrides' => new ParameterOverrides(['TEST_KEY' => 'TEST_VALUE']),
+ 'source_bucket_name' => 'source',
+ 'cache_bucket_name' => 'cache',
+ ],
+ ]);
+
+ $imageStorage = Mockery::mock(ImageStorageInterface::class);
+
+ $imageStorage->shouldReceive('getName')
+ ->withNoArgs()
+ ->andReturn('full');
+
+ $imageStorage->shouldReceive('getConfig')
+ ->once()
+ ->withNoArgs()
+ ->andReturn(new Config([
+ ConfigInterface::BASE_PATH => 'images',
+ Config::ENCODE_QUALITY => 80,
+ Config::ALLOWED_PIXEL_DENSITY => [1.0, 2.0, 2.5, 3.0],
+ Config::SIGNATURE_KEY => 'abc',
+ ]));
+
+ $imageStorage->shouldReceive('getNoImageConfig')
+ ->once()
+ ->withNoArgs()
+ ->andReturn(new NoImageConfig(
+ 'noimage/noimage.png',
+ [
+ 'test' => 'test/noimage.png',
+ ],
+ [
+ 'test' => '^test\/',
+ ]
+ ));
+
+ $filename = $this->outputDir . '/test_stack/samconfig.toml';
+
+ try {
+ $generator->generate($imageStorage);
+
+ Assert::true(file_exists($filename));
+ Assert::same(file_get_contents(__DIR__ . '/config.fullConfiguration.toml'), file_get_contents($filename));
+ } finally {
+ @unlink($filename);
+ }
+ }
+
+ protected function setUp(): void
+ {
+ $this->outputDir = sys_get_temp_dir() . '/' . uniqid('68publishers:ImageStorage:SamConfigGeneratorTest', true);
+ }
+
+ protected function tearDown(): void
+ {
+ Mockery::close();
+ }
+
+ private function createFilesystemInMemoryMountManager(): MountManager
+ {
+ return new MountManager([
+ ImagePersisterInterface::FILESYSTEM_NAME_SOURCE => new Filesystem(
+ new InMemoryFilesystemAdapter()
+ ),
+ ImagePersisterInterface::FILESYSTEM_NAME_CACHE => new Filesystem(
+ new InMemoryFilesystemAdapter()
+ ),
+ ]);
+ }
+
+ private function createFilesystemS3MountManager(): MountManager
+ {
+ return new MountManager([
+ ImagePersisterInterface::FILESYSTEM_NAME_SOURCE => new Filesystem(
+ new AwsS3V3Adapter(
+ Mockery::mock(S3ClientInterface::class),
+ 'source_bucket'
+ )
+ ),
+ ImagePersisterInterface::FILESYSTEM_NAME_CACHE => new Filesystem(
+ new AwsS3V3Adapter(
+ Mockery::mock(S3ClientInterface::class),
+ 'cache_bucket'
+ )
+ ),
+ ]);
+ }
+}
+
+(new SamConfigGeneratorTest())->run();
diff --git a/tests/Bridge/ImageStorageLambda/config.fullConfiguration.toml b/tests/Bridge/ImageStorageLambda/config.fullConfiguration.toml
new file mode 100644
index 0000000..7e7dacd
--- /dev/null
+++ b/tests/Bridge/ImageStorageLambda/config.fullConfiguration.toml
@@ -0,0 +1,11 @@
+# Generated by 68publishers/image-storage
+version = 2.5
+
+[default.deploy.parameters]
+stack_name = "test_stack"
+s3_bucket = "test_bucket"
+s3_prefix = "test_prefix"
+region = "west"
+confirm_changeset = true
+capabilities = "CAPABILITY_NAMED_IAM"
+parameter_overrides = "TEST_KEY=\"TEST_VALUE\" BasePath=\"images\" ModifierSeparator=\",\" ModifierAssigner=\":\" VersionParameterName=\"_v\" SignatureParameterName=\"_s\" SignatureKey=\"abc\" SignatureAlgorithm=\"sha256\" AllowedPixelDensity=\"1,2,2.5,3\" AllowedResolutions=\"\" AllowedQualities=\"\" EncodeQuality=\"80\" SourceBucketName=\"source\" CacheBucketName=\"cache\" CacheMaxAge=\"31536000\" NoImages=\"default::noimage/noimage.png,test::test/noimage.png\" NoImagePatterns=\"test::^test\\/\""
diff --git a/tests/Bridge/ImageStorageLambda/config.minimalConfiguration.toml b/tests/Bridge/ImageStorageLambda/config.minimalConfiguration.toml
new file mode 100644
index 0000000..51f8264
--- /dev/null
+++ b/tests/Bridge/ImageStorageLambda/config.minimalConfiguration.toml
@@ -0,0 +1,11 @@
+# Generated by 68publishers/image-storage
+version = 1.0
+
+[default.deploy.parameters]
+stack_name = "minimal"
+s3_bucket = "test_bucket"
+s3_prefix = "minimal"
+region = "west"
+confirm_changeset = false
+capabilities = "CAPABILITY_IAM"
+parameter_overrides = "BasePath=\"\" ModifierSeparator=\",\" ModifierAssigner=\":\" VersionParameterName=\"_v\" SignatureParameterName=\"_s\" SignatureKey=\"\" SignatureAlgorithm=\"sha256\" AllowedPixelDensity=\"\" AllowedResolutions=\"\" AllowedQualities=\"\" EncodeQuality=\"90\" SourceBucketName=\"source_bucket\" CacheBucketName=\"cache_bucket\" CacheMaxAge=\"31536000\" NoImages=\"\" NoImagePatterns=\"\""
diff --git a/tests/Bridge/Nette/DI/ContainerFactory.php b/tests/Bridge/Nette/DI/ContainerFactory.php
new file mode 100644
index 0000000..0d30e82
--- /dev/null
+++ b/tests/Bridge/Nette/DI/ContainerFactory.php
@@ -0,0 +1,45 @@
+ $configFiles
+ */
+ public static function create(string|array $configFiles): Container
+ {
+ $tempDir = sys_get_temp_dir() . '/' . uniqid('68publishers:ImageStorage', true);
+ $backtrace = debug_backtrace();
+
+ Helpers::purge($tempDir);
+
+ $configurator = new Configurator();
+ $configurator->setTempDirectory($tempDir);
+ $configurator->setDebugMode(true);
+
+ $configurator->addParameters([
+ 'cwd' => dirname($backtrace[0]['file']),
+ ]);
+
+ foreach ((array) $configFiles as $configFile) {
+ $configurator->addConfig($configFile);
+ }
+
+ return $configurator->createContainer();
+ }
+}
diff --git a/tests/Bridge/Nette/DI/ImageStorageExtensionTest.php b/tests/Bridge/Nette/DI/ImageStorageExtensionTest.php
new file mode 100644
index 0000000..6d6eb71
--- /dev/null
+++ b/tests/Bridge/Nette/DI/ImageStorageExtensionTest.php
@@ -0,0 +1,480 @@
+ ContainerFactory::create(__DIR__ . '/config/ImageStorage/config.error.missingFileStorageExtension.neon'),
+ RuntimeException::class,
+ 'The extension SixtyEightPublishers\ImageStorage\Bridge\Nette\DI\ImageStorageExtension can be used only with SixtyEightPublishers\FileStorage\Bridge\Nette\DI\FileStorageExtension.'
+ );
+ }
+
+ public function testExceptionShouldBeThrownIfImageStorageIsNotDefinedInFileStorage(): void
+ {
+ Assert::exception(
+ static fn () => ContainerFactory::create(__DIR__ . '/config/ImageStorage/config.error.missingDefinitionInFileStorageExtension.neon'),
+ RuntimeException::class,
+ 'Missing definition for a storage with the name "missing_in_file_storage" in the configuration of the extension SixtyEightPublishers\FileStorage\Bridge\Nette\DI\FileStorageExtension.'
+ );
+ }
+
+ public function testExtensionShouldBeIntegratedWithMinimalConfiguration(): void
+ {
+ $container = ContainerFactory::create(__DIR__ . '/config/ImageStorage/config.minimal.neon');
+
+ $this->assertImageManager($container, ImageStorageExtension::DRIVER_GD);
+ $this->assertStorageCleaner($container);
+ $this->assertImageStorage(
+ container: $container,
+ imageServerFactoryType: LocalImageServerFactory::class,
+ signatureStrategyType: null,
+ configOptions: [
+ ConfigInterface::BASE_PATH => 'images',
+ ConfigInterface::HOST => null,
+ Config::MODIFIER_SEPARATOR => ',',
+ Config::MODIFIER_ASSIGNER => ':',
+ ConfigInterface::VERSION_PARAMETER_NAME => '_v',
+ Config::SIGNATURE_PARAMETER_NAME => '_s',
+ Config::SIGNATURE_KEY => null,
+ Config::SIGNATURE_ALGORITHM => 'sha256',
+ Config::ALLOWED_PIXEL_DENSITY => [],
+ Config::ALLOWED_RESOLUTIONS => [],
+ Config::ALLOWED_QUALITIES => [],
+ Config::ENCODE_QUALITY => 90,
+ Config::CACHE_MAX_AGE => 31536000,
+ ]
+ );
+ $this->assertModifierFacade(
+ container: $container,
+ modifierTypes: [
+ Modifier\Original::class,
+ Modifier\Height::class,
+ Modifier\Width::class,
+ Modifier\AspectRatio::class,
+ Modifier\Fit::class,
+ Modifier\PixelDensity::class,
+ Modifier\Orientation::class,
+ Modifier\Quality::class,
+ ],
+ applicatorTypes: [
+ Applicator\Orientation::class,
+ Applicator\Resize::class,
+ Applicator\Format::class,
+ ],
+ validatorTypes: [
+ Validator\AllowedResolutionValidator::class,
+ Validator\AllowedPixelDensityValidator::class,
+ Validator\AllowedQualityValidator::class,
+ ],
+ presets: []
+ );
+ $this->assertNoImageConfig(
+ container: $container,
+ defaultPath: null,
+ paths: [],
+ patterns: []
+ );
+ }
+
+ public function testExtensionShouldBeIntegratedWithImagickDriver(): void
+ {
+ $container = ContainerFactory::create(__DIR__ . '/config/ImageStorage/config.withImagickDriver.neon');
+
+ $this->assertImageManager($container, ImageStorageExtension::DRIVER_IMAGICK);
+ }
+
+ public function testExtensionShouldBeIntegratedWith68publishersImagickDriver(): void
+ {
+ $container = ContainerFactory::create(__DIR__ . '/config/ImageStorage/config.with68publishersImagickDriver.neon');
+
+ $this->assertImageManager($container, SixtyEightPublishersImagickDriver::class);
+ }
+
+ public function testExtensionShouldBeIntegratedWithExternalImageServer(): void
+ {
+ $container = ContainerFactory::create(__DIR__ . '/config/ImageStorage/config.withExternalImageServer.neon');
+
+ $this->assertImageStorage(
+ container: $container,
+ imageServerFactoryType: ExternalImageServerFactory::class,
+ signatureStrategyType: null,
+ configOptions: [
+ ConfigInterface::BASE_PATH => '',
+ ConfigInterface::HOST => 'https://www.example.com',
+ Config::MODIFIER_SEPARATOR => ',',
+ Config::MODIFIER_ASSIGNER => ':',
+ ConfigInterface::VERSION_PARAMETER_NAME => '_v',
+ Config::SIGNATURE_PARAMETER_NAME => '_s',
+ Config::SIGNATURE_KEY => null,
+ Config::SIGNATURE_ALGORITHM => 'sha256',
+ Config::ALLOWED_PIXEL_DENSITY => [],
+ Config::ALLOWED_RESOLUTIONS => [],
+ Config::ALLOWED_QUALITIES => [],
+ Config::ENCODE_QUALITY => 90,
+ Config::CACHE_MAX_AGE => 31536000,
+ ]
+ );
+ }
+
+ public function testExtensionShouldBeIntegratedWithSignatureStrategy(): void
+ {
+ $container = ContainerFactory::create(__DIR__ . '/config/ImageStorage/config.withSignatureStrategy.neon');
+
+ $this->assertImageStorage(
+ container: $container,
+ imageServerFactoryType: LocalImageServerFactory::class,
+ signatureStrategyType: SignatureStrategy::class,
+ configOptions: [
+ ConfigInterface::BASE_PATH => 'images',
+ ConfigInterface::HOST => null,
+ Config::MODIFIER_SEPARATOR => ',',
+ Config::MODIFIER_ASSIGNER => ':',
+ ConfigInterface::VERSION_PARAMETER_NAME => '_v',
+ Config::SIGNATURE_PARAMETER_NAME => '_s',
+ Config::SIGNATURE_KEY => 'abc',
+ Config::SIGNATURE_ALGORITHM => 'sha256',
+ Config::ALLOWED_PIXEL_DENSITY => [],
+ Config::ALLOWED_RESOLUTIONS => [],
+ Config::ALLOWED_QUALITIES => [],
+ Config::ENCODE_QUALITY => 90,
+ Config::CACHE_MAX_AGE => 31536000,
+ ]
+ );
+ }
+
+ public function testExtensionShouldBeIntegratedWithCustomModifiersAndApplicatorsAndValidatorsAndPresets(): void
+ {
+ $container = ContainerFactory::create(__DIR__ . '/config/ImageStorage/config.withModifiersAndApplicatorsAndValidatorsAndPresets.neon');
+
+ $this->assertModifierFacade(
+ container: $container,
+ modifierTypes: [
+ Modifier\Original::class,
+ Modifier\Height::class,
+ Modifier\Width::class,
+ Modifier\AspectRatio::class,
+ Modifier\Fit::class,
+ Modifier\PixelDensity::class,
+ Modifier\Orientation::class,
+ Modifier\Quality::class,
+ TestModifier::class,
+ ],
+ applicatorTypes: [
+ TestApplicator::class,
+ Applicator\Orientation::class,
+ Applicator\Resize::class,
+ Applicator\Format::class,
+ ],
+ validatorTypes: [
+ TestValidator::class,
+ ],
+ presets: [
+ 'small' => [
+ 'w' => 100,
+ 'ar' => '2x1',
+ ],
+ 'huge' => [
+ 'w' => 1000,
+ 'ar' => '16x9',
+ ],
+ 'rotated' => [
+ 'o' => 180,
+ ],
+ ]
+ );
+ }
+
+ public function testExtensionShouldBeIntegratedWithNoImageOptions(): void
+ {
+ $container = ContainerFactory::create(__DIR__ . '/config/ImageStorage/config.withNoImageOptions.neon');
+
+ $this->assertNoImageConfig(
+ container: $container,
+ defaultPath: 'nomiage/noimage.png',
+ paths: [
+ 'test' => 'test/noimage.png',
+ ],
+ patterns: [
+ 'test' => '^test\/',
+ ]
+ );
+ }
+
+ public function testCleanCommandConfiguratorShouldBeRegisteredIfConsoleExtensionIsRegistered(): void
+ {
+ $container = ContainerFactory::create(__DIR__ . '/config/ImageStorage/config.withConsoleExtension.neon');
+
+ $this->assertCleanCommandConfigurator($container, [
+ BaseCleanCommandConfigurator::class,
+ ImageStorageCleanCommandConfigurator::class,
+ ]);
+ }
+
+ protected function tearDown(): void
+ {
+ # save manually partial code coverage to free memory
+ if (Collector::isStarted()) {
+ Collector::save();
+ }
+ }
+
+ private function assertImageManager(Container $container, string $driver): void
+ {
+ $imageManager = $container->getByType(ImageManager::class);
+ assert($imageManager instanceof ImageManager);
+
+ Assert::hasKey('driver', $imageManager->config);
+
+ $managerDriver = $imageManager->config['driver'];
+
+ if (is_string($managerDriver)) {
+ Assert::same($driver, $managerDriver);
+ } else {
+ Assert::type($driver, $managerDriver);
+ }
+ }
+
+ private function assertStorageCleaner(Container $container): void
+ {
+ $cleaner = $container->getByType(StorageCleanerInterface::class);
+
+ Assert::type(StorageCleaner::class, $cleaner);
+ }
+
+ private function assertCleanCommandConfigurator(Container $container, array $configuratorTypes): void
+ {
+ $configurator = $container->getByType(CleanCommandConfiguratorInterface::class);
+
+ Assert::type(CleanCommandConfiguratorRegistry::class, $configurator);
+ assert($configurator instanceof CleanCommandConfiguratorRegistry);
+
+ call_user_func(Closure::bind(
+ static function () use ($configurator, $configuratorTypes): void {
+ Assert::same(count($configuratorTypes), count($configurator->configurators));
+
+ foreach ($configuratorTypes as $index => $configuratorType) {
+ Assert::type($configuratorType, $configurator->configurators[$index]);
+ }
+ },
+ null,
+ CleanCommandConfiguratorRegistry::class
+ ));
+ }
+
+ private function assertImageStorage(
+ Container $container,
+ string $imageServerFactoryType,
+ ?string $signatureStrategyType,
+ array $configOptions,
+ ): void {
+ $provider = $container->getByType(FileStorageProviderInterface::class);
+ assert($provider instanceof FileStorageProvider);
+
+ $imageStorage = $provider->get('images');
+ $mountManager = $imageStorage->getFilesystem();
+
+ Assert::type(ImageStorage::class, $imageStorage);
+ Assert::type(MountManager::class, $mountManager);
+
+ assert($mountManager instanceof MountManager);
+
+ $this->assertMountManager($mountManager);
+
+ call_user_func(Closure::bind(
+ static function () use ($imageStorage, $configOptions, $imageServerFactoryType, $signatureStrategyType): void {
+ assert($imageStorage instanceof ImageStorage);
+
+ Assert::type(ResourceFactory::class, $imageStorage->resourceFactory);
+ Assert::type(LinkGenerator::class, $imageStorage->linkGenerator);
+ Assert::type(NoImageResolver::class, $imageStorage->noImageResolver);
+ Assert::type(InfoFactory::class, $imageStorage->infoFactory);
+ Assert::type($imageServerFactoryType, $imageStorage->imageServerFactory);
+
+ if (null === $signatureStrategyType) {
+ Assert::null($imageStorage->linkGenerator->getSignatureStrategy());
+ } else {
+ Assert::type($signatureStrategyType, $imageStorage->linkGenerator->getSignatureStrategy());
+ }
+
+ $config = $imageStorage->getConfig();
+
+ call_user_func(Closure::bind(
+ static function () use ($config, $configOptions): void {
+ Assert::same($configOptions, $config->config);
+ },
+ null,
+ Config::class
+ ));
+ },
+ null,
+ ImageStorage::class
+ ));
+ }
+
+ private function assertMountManager(MountManager $filesystem): void
+ {
+ $assertInMemoryFilesystem = function (Filesystem $filesystem, array $configOptions): void {
+ $this->assertInMemoryFilesystem($filesystem, $configOptions);
+ };
+
+ call_user_func(Closure::bind(
+ static function () use ($filesystem, $assertInMemoryFilesystem): void {
+ $filesystems = $filesystem->filesystems;
+
+ Assert::hasKey(ImagePersisterInterface::FILESYSTEM_NAME_SOURCE, $filesystems);
+ Assert::hasKey(ImagePersisterInterface::FILESYSTEM_NAME_CACHE, $filesystems);
+
+ $sourceFs = $filesystems[ImagePersisterInterface::FILESYSTEM_NAME_SOURCE];
+ $cacheFs = $filesystems[ImagePersisterInterface::FILESYSTEM_NAME_CACHE];
+
+ Assert::type(Filesystem::class, $sourceFs);
+ Assert::type(Filesystem::class, $cacheFs);
+
+ $assertInMemoryFilesystem($sourceFs, [
+ FlysystemConfig::OPTION_VISIBILITY => Visibility::PRIVATE,
+ FlysystemConfig::OPTION_DIRECTORY_VISIBILITY => Visibility::PRIVATE,
+ ]);
+ $assertInMemoryFilesystem($cacheFs, [
+ FlysystemConfig::OPTION_VISIBILITY => Visibility::PUBLIC,
+ FlysystemConfig::OPTION_DIRECTORY_VISIBILITY => Visibility::PUBLIC,
+ ]);
+ },
+ null,
+ MountManager::class
+ ));
+ }
+
+ private function assertInMemoryFilesystem(Filesystem $filesystem, array $configOptions): void
+ {
+ Assert::type(InMemoryFilesystemAdapter::class, $filesystem->getAdapter());
+
+ $configProperty = new ReflectionProperty(FlysystemFilesystem::class, 'config');
+ $config = $configProperty->getValue($filesystem);
+ assert($config instanceof FlysystemConfig);
+
+ foreach ($configOptions as $opt => $value) {
+ Assert::same($value, $config->get($opt));
+ }
+ }
+
+ private function assertModifierFacade(
+ Container $container,
+ array $modifierTypes,
+ array $applicatorTypes,
+ array $validatorTypes,
+ array $presets,
+ ): void {
+ $modifierFacade = $container->getService('image_storage.modifier_facade.images');
+ assert($modifierFacade instanceof ModifierFacade);
+
+ call_user_func(Closure::bind(
+ static function () use ($modifierFacade, $applicatorTypes, $validatorTypes, $presets): void {
+ Assert::same($applicatorTypes, array_map(
+ static fn (ModifierApplicatorInterface $applicator): string => get_class($applicator),
+ $modifierFacade->applicators
+ ));
+
+ Assert::same($validatorTypes, array_map(
+ static fn (ValidatorInterface $validator): string => get_class($validator),
+ $modifierFacade->validators
+ ));
+
+ $presetCollection = $modifierFacade->presetCollection;
+ Assert::type(PresetCollection::class, $presetCollection);
+
+ call_user_func(Closure::bind(
+ static function () use ($presetCollection, $presets): void {
+ Assert::same($presets, $presetCollection->presets);
+ },
+ null,
+ PresetCollection::class
+ ));
+ },
+ null,
+ ModifierFacade::class
+ ));
+
+ $modifiers = array_values(iterator_to_array($modifierFacade->getModifierCollection()));
+
+ Assert::same($modifierTypes, array_map(
+ static fn (ModifierInterface $modifier): string => get_class($modifier),
+ $modifiers
+ ));
+ }
+
+ private function assertNoImageConfig(Container $container, ?string $defaultPath, array $paths, array $patterns): void
+ {
+ $noImageConfig = $container->getService('image_storage.no_image_config.images');
+ assert($noImageConfig instanceof NoImageConfig);
+
+ Assert::same($defaultPath, $noImageConfig->getDefaultPath());
+ Assert::same($paths, $noImageConfig->getPaths());
+ Assert::same($patterns, $noImageConfig->getPatterns());
+ }
+}
+
+(new ImageStorageExtensionTest())->run();
diff --git a/tests/Bridge/Nette/DI/ImageStorageLambdaExtensionTest.phpt b/tests/Bridge/Nette/DI/ImageStorageLambdaExtensionTest.phpt
new file mode 100644
index 0000000..addf145
--- /dev/null
+++ b/tests/Bridge/Nette/DI/ImageStorageLambdaExtensionTest.phpt
@@ -0,0 +1,89 @@
+ ContainerFactory::create(__DIR__ . '/config/ImageStorageLambda/config.error.missingImageStorageExtension.neon'),
+ RuntimeException::class,
+ "The extension SixtyEightPublishers\ImageStorage\Bridge\Nette\DI\ImageStorageLambdaExtension can be used only with SixtyEightPublishers\ImageStorage\Bridge\Nette\DI\ImageStorageExtension."
+ );
+ }
+
+ public function testExtensionShouldBeIntegrated(): void
+ {
+ $container = ContainerFactory::create(__DIR__ . '/config/ImageStorageLambda/config.neon');
+ $application = $container->getByType(Application::class);
+ $samConfigGenerator = $container->getByType(SamConfigGeneratorInterface::class);
+ assert($application instanceof Application && $samConfigGenerator instanceof SamConfigGenerator);
+
+ $command = $application->get('image-storage:lambda:dump-config');
+
+ Assert::type(DumpLambdaConfigCommand::class, $command);
+
+ call_user_func(Closure::bind(
+ static function () use ($samConfigGenerator): void {
+ Assert::same(__DIR__ . '/lambda', $samConfigGenerator->outputDir);
+ Assert::same([
+ 'images' => [
+ 'stack_name' => null,
+ 'version' => 1.0,
+ 's3_bucket' => 'test_bucket',
+ 's3_prefix' => null,
+ 'region' => 'west',
+ 'confirm_changeset' => false,
+ 'capabilities' => 'CAPABILITY_IAM',
+ 'parameter_overrides' => [],
+ 'source_bucket_name' => null,
+ 'cache_bucket_name' => null,
+ ],
+ 'images2' => [
+ 'stack_name' => 'test_stack',
+ 'version' => 2.5,
+ 's3_bucket' => 'test_bucket',
+ 's3_prefix' => 'test_prefix',
+ 'region' => 'west',
+ 'confirm_changeset' => true,
+ 'capabilities' => 'CAPABILITY_NAMED_IAM',
+ 'parameter_overrides' => [
+ 'TestKey' => 'TestValue',
+ ],
+ 'source_bucket_name' => 'source',
+ 'cache_bucket_name' => 'cache',
+ ],
+ ], $samConfigGenerator->configs);
+ },
+ null,
+ SamConfigGenerator::class
+ ));
+ }
+
+ protected function tearDown(): void
+ {
+ # save manually partial code coverage to free memory
+ if (Collector::isStarted()) {
+ Collector::save();
+ }
+ }
+}
+
+(new ImageStorageLambdaExtensionTest())->run();
diff --git a/tests/Bridge/Nette/DI/ImageStorageLatteExtensionTest.phpt b/tests/Bridge/Nette/DI/ImageStorageLatteExtensionTest.phpt
new file mode 100644
index 0000000..96824e9
--- /dev/null
+++ b/tests/Bridge/Nette/DI/ImageStorageLatteExtensionTest.phpt
@@ -0,0 +1,81 @@
+ ContainerFactory::create(__DIR__ . '/config/ImageStorageLatte/config.error.missingImageStorageExtension.neon'),
+ RuntimeException::class,
+ "The extension SixtyEightPublishers\ImageStorage\Bridge\Nette\DI\ImageStorageLatteExtension can be used only with SixtyEightPublishers\ImageStorage\Bridge\Nette\DI\ImageStorageExtension."
+ );
+ }
+
+ /**
+ * @dataProvider getTestingCodes
+ */
+ public function testLatteExtensionFunctions(string $latteCode, string $htmlCode): void
+ {
+ $container = ContainerFactory::create(__DIR__ . '/config/ImageStorageLatte/config.neon');
+
+ $this->assertLatte($container, [
+ $latteCode => $htmlCode,
+ ]);
+ }
+
+ public function getTestingCodes(): array
+ {
+ return [
+ ['
', '
'],
+ ['
', '
'],
+ ['
', '
'],
+ ['
', '
'],
+ ['
', '
'],
+
+ ['
', '
'],
+ ['
', '
'],
+ ['
', '
'],
+ ['
', '
'],
+ ];
+ }
+
+ protected function tearDown(): void
+ {
+ # save manually partial code coverage to free memory
+ if (Collector::isStarted()) {
+ Collector::save();
+ }
+ }
+
+ private function assertLatte(Container $container, array $assertions, array $params = []): void
+ {
+ $latteFactory = $container->getByType(LatteFactory::class);
+ assert($latteFactory instanceof LatteFactory);
+ $engine = $latteFactory->create();
+
+ $engine->setLoader(new StringLoader());
+
+ foreach ($assertions as $latteCode => $expected) {
+ $rendered = $engine->renderToString($latteCode, $params);
+
+ Assert::contains($expected, $rendered);
+ }
+ }
+}
+
+(new ImageStorageLatteExtensionTest())->run();
diff --git a/tests/Bridge/Nette/DI/config/ImageStorage/config.error.missingDefinitionInFileStorageExtension.neon b/tests/Bridge/Nette/DI/config/ImageStorage/config.error.missingDefinitionInFileStorageExtension.neon
new file mode 100644
index 0000000..c0d4f67
--- /dev/null
+++ b/tests/Bridge/Nette/DI/config/ImageStorage/config.error.missingDefinitionInFileStorageExtension.neon
@@ -0,0 +1,20 @@
+extensions:
+ file_storage: SixtyEightPublishers\FileStorage\Bridge\Nette\DI\FileStorageExtension(%cwd%)
+ image_storage: SixtyEightPublishers\ImageStorage\Bridge\Nette\DI\ImageStorageExtension
+
+file_storage:
+ storages:
+ images:
+ config:
+ base_path: images
+ filesystem:
+ adapter: League\Flysystem\InMemory\InMemoryFilesystemAdapter
+
+image_storage:
+ storages:
+ images:
+ source_filesystem:
+ adapter: League\Flysystem\InMemory\InMemoryFilesystemAdapter
+ missing_in_file_storage:
+ source_filesystem:
+ adapter: League\Flysystem\InMemory\InMemoryFilesystemAdapter
diff --git a/tests/Bridge/Nette/DI/config/ImageStorage/config.error.missingFileStorageExtension.neon b/tests/Bridge/Nette/DI/config/ImageStorage/config.error.missingFileStorageExtension.neon
new file mode 100644
index 0000000..8ba9695
--- /dev/null
+++ b/tests/Bridge/Nette/DI/config/ImageStorage/config.error.missingFileStorageExtension.neon
@@ -0,0 +1,8 @@
+extensions:
+ image_storage: SixtyEightPublishers\ImageStorage\Bridge\Nette\DI\ImageStorageExtension
+
+image_storage:
+ storages:
+ images:
+ source_filesystem:
+ adapter: League\Flysystem\InMemory\InMemoryFilesystemAdapter
diff --git a/tests/Bridge/Nette/DI/config/ImageStorage/config.minimal.neon b/tests/Bridge/Nette/DI/config/ImageStorage/config.minimal.neon
new file mode 100644
index 0000000..27b39d2
--- /dev/null
+++ b/tests/Bridge/Nette/DI/config/ImageStorage/config.minimal.neon
@@ -0,0 +1,17 @@
+extensions:
+ file_storage: SixtyEightPublishers\FileStorage\Bridge\Nette\DI\FileStorageExtension(%cwd%)
+ image_storage: SixtyEightPublishers\ImageStorage\Bridge\Nette\DI\ImageStorageExtension
+
+file_storage:
+ storages:
+ images:
+ config:
+ base_path: images
+ filesystem:
+ adapter: League\Flysystem\InMemory\InMemoryFilesystemAdapter
+
+image_storage:
+ storages:
+ images:
+ source_filesystem:
+ adapter: League\Flysystem\InMemory\InMemoryFilesystemAdapter
diff --git a/tests/Bridge/Nette/DI/config/ImageStorage/config.with68publishersImagickDriver.neon b/tests/Bridge/Nette/DI/config/ImageStorage/config.with68publishersImagickDriver.neon
new file mode 100644
index 0000000..adbc611
--- /dev/null
+++ b/tests/Bridge/Nette/DI/config/ImageStorage/config.with68publishersImagickDriver.neon
@@ -0,0 +1,18 @@
+extensions:
+ file_storage: SixtyEightPublishers\FileStorage\Bridge\Nette\DI\FileStorageExtension(%cwd%)
+ image_storage: SixtyEightPublishers\ImageStorage\Bridge\Nette\DI\ImageStorageExtension
+
+file_storage:
+ storages:
+ images:
+ config:
+ base_path: images
+ filesystem:
+ adapter: League\Flysystem\InMemory\InMemoryFilesystemAdapter
+
+image_storage:
+ driver: 68publishers.imagick
+ storages:
+ images:
+ source_filesystem:
+ adapter: League\Flysystem\InMemory\InMemoryFilesystemAdapter
diff --git a/tests/Bridge/Nette/DI/config/ImageStorage/config.withConsoleExtension.neon b/tests/Bridge/Nette/DI/config/ImageStorage/config.withConsoleExtension.neon
new file mode 100644
index 0000000..e778c47
--- /dev/null
+++ b/tests/Bridge/Nette/DI/config/ImageStorage/config.withConsoleExtension.neon
@@ -0,0 +1,18 @@
+extensions:
+ file_storage: SixtyEightPublishers\FileStorage\Bridge\Nette\DI\FileStorageExtension(%cwd%)
+ file_storage.console: SixtyEightPublishers\FileStorage\Bridge\Nette\DI\FileStorageConsoleExtension
+ image_storage: SixtyEightPublishers\ImageStorage\Bridge\Nette\DI\ImageStorageExtension
+
+file_storage:
+ storages:
+ images:
+ config:
+ base_path: images
+ filesystem:
+ adapter: League\Flysystem\InMemory\InMemoryFilesystemAdapter
+
+image_storage:
+ storages:
+ images:
+ source_filesystem:
+ adapter: League\Flysystem\InMemory\InMemoryFilesystemAdapter
diff --git a/tests/Bridge/Nette/DI/config/ImageStorage/config.withExternalImageServer.neon b/tests/Bridge/Nette/DI/config/ImageStorage/config.withExternalImageServer.neon
new file mode 100644
index 0000000..1794a5e
--- /dev/null
+++ b/tests/Bridge/Nette/DI/config/ImageStorage/config.withExternalImageServer.neon
@@ -0,0 +1,18 @@
+extensions:
+ file_storage: SixtyEightPublishers\FileStorage\Bridge\Nette\DI\FileStorageExtension(%cwd%)
+ image_storage: SixtyEightPublishers\ImageStorage\Bridge\Nette\DI\ImageStorageExtension
+
+file_storage:
+ storages:
+ images:
+ config:
+ host: https://www.example.com
+ filesystem:
+ adapter: League\Flysystem\InMemory\InMemoryFilesystemAdapter
+
+image_storage:
+ storages:
+ images:
+ source_filesystem:
+ adapter: League\Flysystem\InMemory\InMemoryFilesystemAdapter
+ server: external
\ No newline at end of file
diff --git a/tests/Bridge/Nette/DI/config/ImageStorage/config.withImagickDriver.neon b/tests/Bridge/Nette/DI/config/ImageStorage/config.withImagickDriver.neon
new file mode 100644
index 0000000..d02a4ea
--- /dev/null
+++ b/tests/Bridge/Nette/DI/config/ImageStorage/config.withImagickDriver.neon
@@ -0,0 +1,18 @@
+extensions:
+ file_storage: SixtyEightPublishers\FileStorage\Bridge\Nette\DI\FileStorageExtension(%cwd%)
+ image_storage: SixtyEightPublishers\ImageStorage\Bridge\Nette\DI\ImageStorageExtension
+
+file_storage:
+ storages:
+ images:
+ config:
+ base_path: images
+ filesystem:
+ adapter: League\Flysystem\InMemory\InMemoryFilesystemAdapter
+
+image_storage:
+ driver: imagick
+ storages:
+ images:
+ source_filesystem:
+ adapter: League\Flysystem\InMemory\InMemoryFilesystemAdapter
diff --git a/tests/Bridge/Nette/DI/config/ImageStorage/config.withModifiersAndApplicatorsAndValidatorsAndPresets.neon b/tests/Bridge/Nette/DI/config/ImageStorage/config.withModifiersAndApplicatorsAndValidatorsAndPresets.neon
new file mode 100644
index 0000000..73fed5d
--- /dev/null
+++ b/tests/Bridge/Nette/DI/config/ImageStorage/config.withModifiersAndApplicatorsAndValidatorsAndPresets.neon
@@ -0,0 +1,34 @@
+extensions:
+ file_storage: SixtyEightPublishers\FileStorage\Bridge\Nette\DI\FileStorageExtension(%cwd%)
+ image_storage: SixtyEightPublishers\ImageStorage\Bridge\Nette\DI\ImageStorageExtension
+
+file_storage:
+ storages:
+ images:
+ config:
+ base_path: images
+ filesystem:
+ adapter: League\Flysystem\InMemory\InMemoryFilesystemAdapter
+
+image_storage:
+ storages:
+ images:
+ source_filesystem:
+ adapter: League\Flysystem\InMemory\InMemoryFilesystemAdapter
+ modifiers:
+ - @default
+ - SixtyEightPublishers\ImageStorage\Tests\Fixtures\TestModifier
+ applicators:
+ - SixtyEightPublishers\ImageStorage\Tests\Fixtures\TestApplicator
+ - @default
+ validators:
+ - SixtyEightPublishers\ImageStorage\Tests\Fixtures\TestValidator
+ presets:
+ small:
+ w: 100
+ ar: 2x1
+ huge:
+ w: 1000
+ ar: 16x9
+ rotated:
+ o: 180
diff --git a/tests/Bridge/Nette/DI/config/ImageStorage/config.withNoImageOptions.neon b/tests/Bridge/Nette/DI/config/ImageStorage/config.withNoImageOptions.neon
new file mode 100644
index 0000000..21dd18f
--- /dev/null
+++ b/tests/Bridge/Nette/DI/config/ImageStorage/config.withNoImageOptions.neon
@@ -0,0 +1,22 @@
+extensions:
+ file_storage: SixtyEightPublishers\FileStorage\Bridge\Nette\DI\FileStorageExtension(%cwd%)
+ image_storage: SixtyEightPublishers\ImageStorage\Bridge\Nette\DI\ImageStorageExtension
+
+file_storage:
+ storages:
+ images:
+ config:
+ base_path: images
+ filesystem:
+ adapter: League\Flysystem\InMemory\InMemoryFilesystemAdapter
+
+image_storage:
+ storages:
+ images:
+ source_filesystem:
+ adapter: League\Flysystem\InMemory\InMemoryFilesystemAdapter
+ no_image:
+ default: nomiage/noimage.png
+ test: test/noimage.png
+ no_image_patterns:
+ test: '^test\/'
diff --git a/tests/Bridge/Nette/DI/config/ImageStorage/config.withSignatureStrategy.neon b/tests/Bridge/Nette/DI/config/ImageStorage/config.withSignatureStrategy.neon
new file mode 100644
index 0000000..179a417
--- /dev/null
+++ b/tests/Bridge/Nette/DI/config/ImageStorage/config.withSignatureStrategy.neon
@@ -0,0 +1,18 @@
+extensions:
+ file_storage: SixtyEightPublishers\FileStorage\Bridge\Nette\DI\FileStorageExtension(%cwd%)
+ image_storage: SixtyEightPublishers\ImageStorage\Bridge\Nette\DI\ImageStorageExtension
+
+file_storage:
+ storages:
+ images:
+ config:
+ base_path: images
+ signature_key: abc
+ filesystem:
+ adapter: League\Flysystem\InMemory\InMemoryFilesystemAdapter
+
+image_storage:
+ storages:
+ images:
+ source_filesystem:
+ adapter: League\Flysystem\InMemory\InMemoryFilesystemAdapter
diff --git a/tests/Bridge/Nette/DI/config/ImageStorageLambda/config.error.missingImageStorageExtension.neon b/tests/Bridge/Nette/DI/config/ImageStorageLambda/config.error.missingImageStorageExtension.neon
new file mode 100644
index 0000000..79e2c86
--- /dev/null
+++ b/tests/Bridge/Nette/DI/config/ImageStorageLambda/config.error.missingImageStorageExtension.neon
@@ -0,0 +1,2 @@
+extensions:
+ image_storage.lambda: SixtyEightPublishers\ImageStorage\Bridge\Nette\DI\ImageStorageLambdaExtension
diff --git a/tests/Bridge/Nette/DI/config/ImageStorageLambda/config.neon b/tests/Bridge/Nette/DI/config/ImageStorageLambda/config.neon
new file mode 100644
index 0000000..bad01fe
--- /dev/null
+++ b/tests/Bridge/Nette/DI/config/ImageStorageLambda/config.neon
@@ -0,0 +1,51 @@
+extensions:
+ file_storage: SixtyEightPublishers\FileStorage\Bridge\Nette\DI\FileStorageExtension(%cwd%)
+ image_storage: SixtyEightPublishers\ImageStorage\Bridge\Nette\DI\ImageStorageExtension
+ image_storage.lambda: SixtyEightPublishers\ImageStorage\Bridge\Nette\DI\ImageStorageLambdaExtension
+
+file_storage:
+ storages:
+ images:
+ config:
+ base_path: images
+ filesystem:
+ adapter: League\Flysystem\InMemory\InMemoryFilesystemAdapter
+ images2:
+ config:
+ base_path: images2
+ filesystem:
+ adapter: League\Flysystem\InMemory\InMemoryFilesystemAdapter
+
+image_storage:
+ storages:
+ images:
+ source_filesystem:
+ adapter: League\Flysystem\InMemory\InMemoryFilesystemAdapter
+ images2:
+ source_filesystem:
+ adapter: League\Flysystem\InMemory\InMemoryFilesystemAdapter
+
+image_storage.lambda:
+ output_dir: %cwd%/lambda
+ stacks:
+ images:
+ s3_bucket: test_bucket
+ region: west
+ images2:
+ stack_name: test_stack
+ version: 2.5
+ s3_bucket: test_bucket
+ s3_prefix: test_prefix
+ region: west
+ confirm_changeset: true
+ capabilities: CAPABILITY_NAMED_IAM
+ parameter_overrides:
+ TestKey: TestValue
+ source_bucket_name: source
+ cache_bucket_name: cache
+
+services:
+ -
+ type: Symfony\Component\Console\Application
+ setup:
+ - addCommands(typed(Symfony\Component\Console\Command\Command))
diff --git a/tests/Bridge/Nette/DI/config/ImageStorageLatte/config.error.missingImageStorageExtension.neon b/tests/Bridge/Nette/DI/config/ImageStorageLatte/config.error.missingImageStorageExtension.neon
new file mode 100644
index 0000000..82b748d
--- /dev/null
+++ b/tests/Bridge/Nette/DI/config/ImageStorageLatte/config.error.missingImageStorageExtension.neon
@@ -0,0 +1,2 @@
+extensions:
+ image_storage.latte: SixtyEightPublishers\ImageStorage\Bridge\Nette\DI\ImageStorageLatteExtension
diff --git a/tests/Bridge/Nette/DI/config/ImageStorageLatte/config.neon b/tests/Bridge/Nette/DI/config/ImageStorageLatte/config.neon
new file mode 100644
index 0000000..a32e318
--- /dev/null
+++ b/tests/Bridge/Nette/DI/config/ImageStorageLatte/config.neon
@@ -0,0 +1,32 @@
+extensions:
+ file_storage: SixtyEightPublishers\FileStorage\Bridge\Nette\DI\FileStorageExtension(%cwd%)
+ image_storage: SixtyEightPublishers\ImageStorage\Bridge\Nette\DI\ImageStorageExtension
+ image_storage.latte: SixtyEightPublishers\ImageStorage\Bridge\Nette\DI\ImageStorageLatteExtension
+
+file_storage:
+ storages:
+ images:
+ config:
+ base_path: images
+ filesystem:
+ adapter: League\Flysystem\InMemory\InMemoryFilesystemAdapter
+ images2:
+ config:
+ base_path: images2
+ filesystem:
+ adapter: League\Flysystem\InMemory\InMemoryFilesystemAdapter
+
+image_storage:
+ storages:
+ images:
+ source_filesystem:
+ adapter: League\Flysystem\InMemory\InMemoryFilesystemAdapter
+ no_image:
+ default: noimage/noimage.png
+ test: test/noimage.png
+ images2:
+ source_filesystem:
+ adapter: League\Flysystem\InMemory\InMemoryFilesystemAdapter
+ no_image:
+ default: noimage/noimage.png
+ test: test/noimage.png
diff --git a/tests/Bridge/Nette/ImageServer/ErrorResponseTest.phpt b/tests/Bridge/Nette/ImageServer/ErrorResponseTest.phpt
new file mode 100644
index 0000000..ad9e374
--- /dev/null
+++ b/tests/Bridge/Nette/ImageServer/ErrorResponseTest.phpt
@@ -0,0 +1,44 @@
+shouldReceive('setCode')
+ ->once()
+ ->with(404)
+ ->andReturnSelf();
+
+ $response = new ErrorResponse($exception);
+ $output = Helpers::capture(static fn () => $response->send($httpRequest, $httpResponse));
+
+ Assert::same($exception, $response->getException());
+ Assert::same('{"code":404,"message":"File not found."}', $output);
+ }
+
+ protected function tearDown(): void
+ {
+ Mockery::close();
+ }
+}
+
+(new ErrorResponseTest())->run();
diff --git a/tests/Bridge/Nette/ImageServer/ImageResponseTest.phpt b/tests/Bridge/Nette/ImageServer/ImageResponseTest.phpt
new file mode 100644
index 0000000..755e59b
--- /dev/null
+++ b/tests/Bridge/Nette/ImageServer/ImageResponseTest.phpt
@@ -0,0 +1,117 @@
+createFilesystem();
+
+ $httpResponse->shouldReceive('setCode')
+ ->once()
+ ->with(404)
+ ->andReturnSelf();
+
+ $response = new ImageResponse($filesystem, 'test/w:100/image.png', 31536000);
+ $output = Helpers::capture(static fn () => $response->send($httpRequest, $httpResponse));
+
+ Assert::same('{"code":404,"message":"Unable to read file."}', $output);
+ }
+
+ public function testErrorResponseShouldBeSentIfFilesystemExceptionIsThrown(): void
+ {
+ $httpResponse = Mockery::mock(IResponse::class);
+ $httpRequest = Mockery::mock(IRequest::class);
+ $filesystem = Mockery::instanceMock($this->createFilesystem([
+ 'test/w:100/image.png' => '... image content ...',
+ ]));
+
+ $filesystem->shouldReceive('mimeType')
+ ->once()
+ ->with('test/w:100/image.png')
+ ->andThrows(UnableToRetrieveMetadata::mimeType('test/w:100/image.png', 'test'));
+
+ $httpResponse->shouldReceive('setCode')
+ ->once()
+ ->with(500)
+ ->andReturnSelf();
+
+ $response = new ImageResponse($filesystem, 'test/w:100/image.png', 31536000);
+ $output = Helpers::capture(static fn () => $response->send($httpRequest, $httpResponse));
+
+ Assert::match('{"code":500,"message":"Filesystem error. %A%"}', $output);
+ }
+
+ public function testImageResponseShouldBeSent(): void
+ {
+ $httpResponse = Mockery::mock(IResponse::class);
+ $httpRequest = Mockery::mock(IRequest::class);
+ $filesystem = $this->createFilesystem([
+ 'test/w:100/image.png' => '... image content ...',
+ ]);
+
+ $httpResponse->shouldReceive('setHeader')
+ ->once()
+ ->with('Content-Type', 'image/png')
+ ->andReturnSelf();
+
+ $httpResponse->shouldReceive('setHeader')
+ ->once()
+ ->with('Content-Length', '21')
+ ->andReturnSelf();
+
+ $httpResponse->shouldReceive('setHeader')
+ ->once()
+ ->with('Cache-Control', 'public, max-age=31536000')
+ ->andReturnSelf();
+
+ $httpResponse->shouldReceive('setHeader')
+ ->once()
+ ->with('Expires', Mockery::type('string'))
+ ->andReturnSelf();
+
+ $response = new ImageResponse($filesystem, 'test/w:100/image.png', 31536000);
+ $output = Helpers::capture(static fn () => $response->send($httpRequest, $httpResponse));
+
+ Assert::match('... image content ...', $output);
+ }
+
+ protected function tearDown(): void
+ {
+ Mockery::close();
+ }
+
+ private function createFilesystem(array $files = []): Filesystem
+ {
+ $filesystem = new Filesystem(
+ new InMemoryFilesystemAdapter(mimeTypeDetector: new ExtensionMimeTypeDetector()),
+ );
+
+ foreach ($files as $filename => $content) {
+ $filesystem->write($filename, $content);
+ }
+
+ return $filesystem;
+ }
+}
+
+(new ImageResponseTest())->run();
diff --git a/tests/Bridge/Nette/ImageServer/RequestTest.phpt b/tests/Bridge/Nette/ImageServer/RequestTest.phpt
new file mode 100644
index 0000000..4195a5a
--- /dev/null
+++ b/tests/Bridge/Nette/ImageServer/RequestTest.phpt
@@ -0,0 +1,31 @@
+getUrlPath());
+ Assert::same('123', $request->getQueryParameter('_v'));
+ Assert::null($request->getQueryParameter('_s'));
+ Assert::same($netteRequest, $request->getOriginalRequest());
+ }
+}
+
+(new RequestTest())->run();
diff --git a/tests/Bridge/Nette/ImageServer/ResponseFactoryTest.phpt b/tests/Bridge/Nette/ImageServer/ResponseFactoryTest.phpt
new file mode 100644
index 0000000..da8e200
--- /dev/null
+++ b/tests/Bridge/Nette/ImageServer/ResponseFactoryTest.phpt
@@ -0,0 +1,63 @@
+shouldReceive('offsetGet')
+ ->once()
+ ->with(Config::CACHE_MAX_AGE)
+ ->andReturn(31536000);
+
+ $response = $factory->createImageResponse($filesystem, 'test/w:100/image.png', $config);
+
+ call_user_func(Closure::bind(
+ static function () use ($response, $filesystem): void {
+ Assert::same($filesystem, $response->filesystemReader);
+ Assert::same('test/w:100/image.png', $response->filePath);
+ Assert::same(31536000, $response->maxAge);
+ },
+ null,
+ ImageResponse::class
+ ));
+ }
+
+ public function testErrorResponseShouldBeCreated(): void
+ {
+ $exception = new ResponseException('File not found.', 404);
+ $config = Mockery::mock(ConfigInterface::class);
+ $factory = new ResponseFactory();
+ $response = $factory->createErrorResponse($exception, $config);
+
+ Assert::same($exception, $response->getException());
+ }
+
+ protected function tearDown(): void
+ {
+ Mockery::close();
+ }
+}
+
+(new ResponseFactoryTest())->run();
diff --git a/tests/Bridge/Symfony/Console/Command/DumpLambdaConfigCommandTest.phpt b/tests/Bridge/Symfony/Console/Command/DumpLambdaConfigCommandTest.phpt
new file mode 100644
index 0000000..a5f0fbb
--- /dev/null
+++ b/tests/Bridge/Symfony/Console/Command/DumpLambdaConfigCommandTest.phpt
@@ -0,0 +1,190 @@
+shouldReceive('getIterator')
+ ->once()
+ ->andReturn(new ArrayIterator([
+ 'default' => $storage1,
+ 'a' => $storage2,
+ 'b' => $storage3,
+ 'c' => $storage4,
+ ]));
+
+ $samConfigGenerator->shouldReceive('canGenerate')
+ ->times(2)
+ ->with($storage2)
+ ->andReturn(true);
+
+ $samConfigGenerator->shouldReceive('canGenerate')
+ ->times(2)
+ ->with($storage3)
+ ->andReturn(true);
+
+ $samConfigGenerator->shouldReceive('canGenerate')
+ ->once()
+ ->with($storage4)
+ ->andReturn(false);
+
+ $samConfigGenerator->shouldReceive('generate')
+ ->once()
+ ->with($storage2)
+ ->andReturn('/config/a.toml');
+
+ $samConfigGenerator->shouldReceive('generate')
+ ->once()
+ ->with($storage3)
+ ->andReturn('/config/b.toml');
+
+ $tester = $this->createCommandTester($samConfigGenerator, $provider);
+
+ $tester->execute([]);
+
+ $display = $tester->getDisplay();
+
+ Assert::same(0, $tester->getStatusCode());
+ Assert::contains('Successfully generated file /config/a.toml', $display);
+ Assert::contains('Successfully generated file /config/b.toml', $display);
+ }
+
+ public function testConfigForSpecifiedImageStoragesShouldBeDumped(): void
+ {
+ $provider = Mockery::mock(FileStorageProviderInterface::class);
+ $storage = Mockery::mock(ImageStorageInterface::class);
+ $samConfigGenerator = Mockery::mock(SamConfigGeneratorInterface::class);
+
+ $provider->shouldReceive('get')
+ ->once()
+ ->with('storage_a')
+ ->andReturn($storage);
+
+ $samConfigGenerator->shouldReceive('canGenerate')
+ ->once()
+ ->with($storage)
+ ->andReturn(true);
+
+ $samConfigGenerator->shouldReceive('generate')
+ ->once()
+ ->with($storage)
+ ->andReturn('/config/storage_a.toml');
+
+ $tester = $this->createCommandTester($samConfigGenerator, $provider);
+
+ $tester->execute([
+ 'storage' => 'storage_a',
+ ]);
+
+ $display = $tester->getDisplay();
+
+ Assert::same(0, $tester->getStatusCode());
+ Assert::contains('Successfully generated file /config/storage_a.toml', $display);
+ }
+
+ public function testExceptionShouldBeThrownIfSpecifiedStorageIsNotInstanceOfImageStorage(): void
+ {
+ $provider = Mockery::mock(FileStorageProviderInterface::class);
+ $storage = Mockery::mock(FileStorageInterface::class);
+ $samConfigGenerator = Mockery::mock(SamConfigGeneratorInterface::class);
+
+ $provider->shouldReceive('get')
+ ->once()
+ ->with('storage_a')
+ ->andReturn($storage);
+
+ $storage->shouldReceive('getName')
+ ->once()
+ ->withNoArgs()
+ ->andReturn('storage_a');
+
+ $tester = $this->createCommandTester($samConfigGenerator, $provider);
+
+ Assert::exception(
+ static fn () => $tester->execute([
+ 'storage' => 'storage_a',
+ ]),
+ InvalidArgumentException::class,
+ 'Storage "storage_a" is not an instance of SixtyEightPublishers\ImageStorage\ImageStorageInterface.'
+ );
+ }
+
+ public function testExceptionShouldBeThrownIfGeneratorCanNotGenerateConfigForSpecifiedStorage(): void
+ {
+ $provider = Mockery::mock(FileStorageProviderInterface::class);
+ $storage = Mockery::mock(ImageStorageInterface::class);
+ $samConfigGenerator = Mockery::mock(SamConfigGeneratorInterface::class);
+
+ $provider->shouldReceive('get')
+ ->once()
+ ->with('storage_a')
+ ->andReturn($storage);
+
+ $samConfigGenerator->shouldReceive('canGenerate')
+ ->once()
+ ->with($storage)
+ ->andReturn(false);
+
+ $storage->shouldReceive('getName')
+ ->once()
+ ->withNoArgs()
+ ->andReturn('storage_a');
+
+ $tester = $this->createCommandTester($samConfigGenerator, $provider);
+
+ Assert::exception(
+ static fn () => $tester->execute([
+ 'storage' => 'storage_a',
+ ]),
+ InvalidArgumentException::class,
+ 'Lambda config for storage "storage_a" can not be generated.'
+ );
+ }
+
+ protected function tearDown(): void
+ {
+ Mockery::close();
+ }
+
+ private function createCommandTester(SamConfigGeneratorInterface $samConfigGenerator, FileStorageProviderInterface $fileStorageProvider): CommandTester
+ {
+ $command = new DumpLambdaConfigCommand($samConfigGenerator, $fileStorageProvider);
+ $application = new Application();
+
+ $application->add($command);
+
+ $command = $application->find('image-storage:lambda:dump-config');
+ assert($command instanceof DumpLambdaConfigCommand);
+
+ return new CommandTester($command);
+ }
+}
+
+(new DumpLambdaConfigCommandTest())->run();
diff --git a/tests/Fixtures/TestApplicator.php b/tests/Fixtures/TestApplicator.php
new file mode 100644
index 0000000..aa84db9
--- /dev/null
+++ b/tests/Fixtures/TestApplicator.php
@@ -0,0 +1,18 @@
+