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). +

+Checks +Coverage Status +Total Downloads +Latest Version +PHP Version +

## 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 @@ +