-
Notifications
You must be signed in to change notification settings - Fork 704
[6.x] Implement AssetTransformers #18869
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
riasvdv
wants to merge
13
commits into
6.x
Choose a base branch
from
feature/transforms
base: 6.x
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
1b4bd94
Implement AssetTransformers
riasvdv d2b2a83
Fix asset transform CI failures
riasvdv 659bbdb
Ensure json settings are decoded
riasvdv 48bf314
No need to invalidate twice
riasvdv aee517c
Require signed private transform URLs
riasvdv d4c72a8
Preserve transform override settings
riasvdv cc7c2b0
Eager load volume default transformers
riasvdv 2a6e3df
Merge branch '6.x' into feature/transforms
riasvdv 0dc87e4
Merge branch '6.x' into feature/transforms
riasvdv 73693c1
Move transformer settings to Filesystems
riasvdv 5d84bc8
Fix missing import
riasvdv 903ef14
Make sure unit tests run with the assumption no updates are needed
riasvdv f93a2d2
Fix legacy test
riasvdv File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,242 @@ | ||
| # Asset Transformers | ||
|
|
||
| Craft 6 includes an asset transformer registry that lets plugins add new ways to transform assets. | ||
|
|
||
| Craft registers its built-in image transformer with the `craft` handle. Plugins can register additional transformer handles for remote image services, video processors, PDF preview generators, text renderers, or any other asset transformation workflow. | ||
|
|
||
| The registry is available through `CraftCms\Cms\Asset\AssetTransforms` or the `CraftCms\Cms\Support\Facades\AssetTransforms` facade. | ||
|
|
||
| ## How Transformers Are Selected | ||
|
|
||
| When an asset transform is requested, Craft resolves the transformer in this order: | ||
|
|
||
| 1. the `transformer` value in the transform array | ||
| 2. the transformer selected by the named image transform | ||
| 3. the asset filesystem's default transformer | ||
| 4. Craft's built-in `craft` transformer | ||
|
|
||
| If a configured transformer handle is missing, Craft logs a warning and falls back to `craft`. | ||
|
|
||
| ```php | ||
| <?php | ||
|
|
||
| $url = $asset->getUrl([ | ||
| 'transformer' => 'my-service', | ||
| 'width' => 1200, | ||
| 'height' => 800, | ||
| 'blur' => 12, | ||
| ]); | ||
| ``` | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. class AssetTransform
{
use ForwardsCalls;
public function __construct(
private Asset $asset,
private AssetTransformer $transformer,
) {
}
public function getUrl(TransformData $transformData): string
{
return $this->transformer->getAssetUrl($this->asset, $transformData);
}
public function __call($method, $parameters): mixed
{
$this->forwardCallTo($this->query, $method, $parameters);
return $this;
}
} |
||
|
|
||
| Unknown transform options are preserved on the transform's `settings` array. Transformers can use the options they support and ignore the rest. | ||
|
|
||
| ## Registering a Transformer | ||
|
|
||
| Register transformers by listening for `AssetTransformersResolving`. | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Laravel Manager driver system instead of event? |
||
|
|
||
| ```php | ||
| <?php | ||
|
|
||
| namespace App\Providers; | ||
|
|
||
| use App\Assets\MyServiceTransformer; | ||
| use CraftCms\Cms\Image\Events\AssetTransformersResolving; | ||
| use Illuminate\Support\Facades\Event; | ||
| use Illuminate\Support\ServiceProvider; | ||
|
|
||
| class AppServiceProvider extends ServiceProvider | ||
| { | ||
| public function boot(): void | ||
| { | ||
| Event::listen(AssetTransformersResolving::class, function (AssetTransformersResolving $event) { | ||
| $event->types[MyServiceTransformer::handle()] = MyServiceTransformer::class; | ||
| }); | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| Handles should be stable, unique, and safe to store in project config. | ||
|
|
||
| ## Implementing a Transformer | ||
|
|
||
| Transformers implement `CraftCms\Cms\Image\Contracts\AssetTransformerInterface`. | ||
|
|
||
| ```php | ||
| <?php | ||
|
|
||
| namespace App\Assets; | ||
|
|
||
| use CraftCms\Cms\Asset\Elements\Asset; | ||
| use CraftCms\Cms\Filesystem\Contracts\FsInterface; | ||
| use CraftCms\Cms\Image\Contracts\AssetTransformerInterface; | ||
| use CraftCms\Cms\Image\Data\ImageTransform; | ||
| use CraftCms\Cms\Shared\Exceptions\NotSupportedException; | ||
| use GraphQL\Type\Definition\Type; | ||
|
|
||
| class MyServiceTransformer implements AssetTransformerInterface | ||
| { | ||
| public static function handle(): string | ||
| { | ||
| return 'my-service'; | ||
| } | ||
|
|
||
| public static function displayName(): string | ||
| { | ||
| return 'My Service'; | ||
| } | ||
|
|
||
| public static function gqlArguments(): array | ||
| { | ||
| return [ | ||
| 'blur' => [ | ||
| 'type' => Type::int(), | ||
| 'description' => 'Blur amount for the transformed asset.', | ||
| ], | ||
| ]; | ||
| } | ||
|
|
||
| public function getTransformUrl(Asset $asset, ImageTransform $imageTransform, bool $immediately): string | ||
| { | ||
| if (! str_starts_with((string) $asset->getMimeType(), 'image/')) { | ||
| throw new NotSupportedException('Only images are supported.'); | ||
| } | ||
|
|
||
| $settings = $imageTransform->settings; | ||
|
|
||
| $sourceUrl = $asset->getUrl(); | ||
|
|
||
| if ($sourceUrl === null) { | ||
| // Use $asset->getStream() or $asset->getCopyOfFile() and upload the | ||
| // source somewhere the transformer can read from. | ||
| throw new NotSupportedException('A public source URL is required.'); | ||
| } | ||
|
|
||
| return 'https://img.example.test/'.http_build_query([ | ||
| 'src' => $sourceUrl, | ||
| 'w' => $imageTransform->width, | ||
| 'h' => $imageTransform->height, | ||
| 'blur' => $settings['blur'] ?? null, | ||
| ]); | ||
| } | ||
|
|
||
| public function invalidateAssetTransforms(Asset $asset): void | ||
| { | ||
| // Purge remote transforms, delete generated files, or clear internal caches. | ||
| } | ||
|
|
||
| public function getTransformString(ImageTransform $imageTransform, bool $ignoreHandle = false): string | ||
| { | ||
| $parts = [ | ||
| 'w' => $imageTransform->width, | ||
| 'h' => $imageTransform->height, | ||
| 'format' => $imageTransform->format, | ||
| 'blur' => $imageTransform->settings['blur'] ?? null, | ||
| ]; | ||
|
|
||
| return http_build_query(array_filter($parts, fn (mixed $value): bool => $value !== null)); | ||
| } | ||
|
|
||
| public function getImageTransformSettingsHtml(ImageTransform $imageTransform, bool $readOnly = false): ?string | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| public function getFilesystemSettingsHtml(FsInterface $filesystem, bool $readOnly = false): ?string | ||
| { | ||
| return null; | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ## Runtime Behavior | ||
|
|
||
| `getTransformUrl()` should return the final URL for the transformed asset. | ||
|
|
||
| Use `NotSupportedException` when the transformer cannot handle the asset or transform options. Craft catches that exception when rendering asset URLs, logs it, and returns `null`. | ||
|
|
||
| The `$immediately` argument tells the transformer whether Craft wants the transform generated before returning the URL. Transformers that delegate to remote services may ignore it when the returned URL is enough to trigger generation externally. | ||
|
|
||
| ## Cache Keys | ||
|
|
||
| `getTransformString()` defines the transformer's canonical cache key. Include only options that affect the generated result. | ||
|
|
||
| This is what lets one transformer support an option like `blur` while another transformer can safely ignore it. Ignored options stay in `ImageTransform::$settings`, but they should not be included in the transform string unless they affect output. | ||
|
|
||
| ## Settings UI | ||
|
|
||
| Named image transforms can store transformer-specific settings. Return CP HTML from `getImageTransformSettingsHtml()` when your transformer needs fields on the named transform edit screen. | ||
|
|
||
| Filesystem defaults can also store transformer-specific settings. Return CP HTML from `getFilesystemSettingsHtml()` when your transformer needs fields on the filesystem edit screen. Craft's built-in `craft` transformer uses those filesystem-scoped settings for its transform filesystem and transform subpath. | ||
|
|
||
| Fields should post under `settings`. | ||
|
|
||
| ```php | ||
| public function getImageTransformSettingsHtml(ImageTransform $imageTransform, bool $readOnly = false): ?string | ||
| { | ||
| $value = htmlspecialchars((string) ($imageTransform->settings['blur'] ?? ''), ENT_QUOTES); | ||
| $disabled = $readOnly ? ' disabled' : ''; | ||
|
|
||
| return <<<HTML | ||
| <div class="field"> | ||
| <div class="heading"> | ||
| <label for="settings-blur">Blur</label> | ||
| </div> | ||
| <div class="input ltr"> | ||
| <input type="number" id="settings-blur" name="settings[blur]" value="{$value}"{$disabled}> | ||
| </div> | ||
| </div> | ||
| HTML; | ||
| } | ||
| ``` | ||
|
|
||
| Craft preserves unsupported settings when the runtime transformer differs from the transformer that originally supplied the settings UI. | ||
|
|
||
| ## GraphQL Arguments | ||
|
|
||
| Transformers may add transform arguments for GraphQL by returning definitions from `gqlArguments()`. | ||
|
|
||
| ```php | ||
| public static function gqlArguments(): array | ||
| { | ||
| return [ | ||
| 'blur' => [ | ||
| 'type' => Type::int(), | ||
| 'description' => 'Blur amount for the transformed asset.', | ||
| ], | ||
| ]; | ||
| } | ||
| ``` | ||
|
|
||
| Argument names must not collide with Craft's built-in transform arguments or arguments registered by another transformer. Craft throws an `ImageTransformException` if a collision is detected. | ||
|
|
||
| ## Filesystem Defaults and Named Transforms | ||
|
|
||
| Filesystems have a default transformer setting. Use that for environment-specific behavior, such as a cloud image service in staging and production but Craft's local transformer in development. The setting can be an environment variable, such as `$ASSET_TRANSFORMER`, whose value is a registered transformer handle. | ||
|
|
||
| Named image transforms can optionally select a transformer. Leave the named transform set to the filesystem default when the transform should follow the asset's filesystem. | ||
|
|
||
| Per-call `transformer` values still take precedence: | ||
|
|
||
| ```twig | ||
| {{ asset.url({ | ||
| transformer: 'my-service', | ||
| transform: 'hero', | ||
| blur: 8, | ||
| }) }} | ||
| ``` | ||
|
|
||
| ## Filesystems With and Without URLs | ||
|
|
||
| Transformers should work with filesystems that have public URLs and filesystems that do not. | ||
|
|
||
| Craft's built-in `craft` transformer stores its transform output target in filesystem-scoped transformer settings. Its settings include the transform filesystem and transform subpath. When no transform filesystem is configured, Craft writes generated transforms to the asset's source filesystem. | ||
|
|
||
| Craft's built-in `craft` transformer returns direct filesystem URLs when possible and signed Craft action URLs for private filesystems. Custom transformers should follow the same principle: return a usable URL regardless of whether the source or configured transform filesystem has public URLs. | ||
|
|
||
| If your transformer needs to read the original asset and `Asset::getUrl()` returns `null`, use `Asset::getStream()` or `Asset::getCopyOfFile()` instead of requiring a public source URL. | ||
|
|
||
| ## Legacy Image Transformers | ||
|
|
||
| The legacy image transformer API remains available through the Yii adapter layer for backwards compatibility. Legacy transformers registered through `craft\services\ImageTransforms::EVENT_REGISTER_IMAGE_TRANSFORMERS` are bridged into the asset transformer registry by their class name. | ||
|
|
||
| New code should implement `AssetTransformerInterface` and register a stable handle instead of registering a legacy image transformer class. | ||
30 changes: 30 additions & 0 deletions
30
resources/templates/_components/asset-transformers/ImageTransformer/filesystem-settings.twig
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| {% import '_includes/forms.twig' as forms %} | ||
|
|
||
| {% namespace 'transformerSettings[craft]' %} | ||
| {{ forms.fsField({ | ||
| label: 'Transform Filesystem'|t('app'), | ||
| instructions: 'Choose which filesystem image transforms should be stored in.'|t('app'), | ||
| id: 'transform-fs-handle', | ||
| name: 'transformFsHandle', | ||
| includeEnvVars: true, | ||
| value: old('transformerSettings.craft.transformFsHandle', settings.transformFsHandle ?? null), | ||
| options: [{label: 'Same as asset filesystem'|t('app'), value: null, data: {hint: ''}}]|merge(craft.cp.getFsOptions()), | ||
| disabled: readOnly, | ||
| showSelectedHint: true, | ||
| selectizeOptions: { | ||
| extraPlugins: false, | ||
| allowEmptyOption: true, | ||
| placeholder: 'Same as asset filesystem'|t('app'), | ||
| }, | ||
| }) }} | ||
|
|
||
| {{ forms.autosuggestField({ | ||
| label: 'Transform Subpath'|t('app'), | ||
| instructions: 'Where transforms should be stored on the filesystem.'|t('app'), | ||
| id: 'transform-subpath', | ||
| name: 'transformSubpath', | ||
| suggestEnvVars: true, | ||
| value: old('transformerSettings.craft.transformSubpath', settings.transformSubpath ?? null), | ||
| disabled: readOnly, | ||
| }) }} | ||
| {% endnamespace %} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
GeneralConfig's default transformer, not just 'craft' (so it can be overridden globally)