davidhirtz / yii2-media
Media management module for admin panel based on Yii 2.0 framework
Package info
github.com/davidhirtz/yii2-media
Type:yii2-extension
pkg:composer/davidhirtz/yii2-media
Requires
- php: ^8.3
- ext-exif: *
- ext-imagick: *
- davidhirtz/yii2-skeleton: ^3.5
- intervention/image: ^4.3
Requires (Dev)
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^12.5
- symfony/browser-kit: ^7.4
- symfony/css-selector: ^7.4
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- dev-main / 3.x-dev
- 3.3.0
- 3.2.0
- 3.1.3
- 3.1.2
- 3.1.1
- 3.1.0
- 3.0.1
- 3.0.0
- v2.x-dev
- 2.3.6
- 2.3.5
- 2.3.4
- 2.3.3
- 2.3.2
- 2.3.1
- 2.3.0
- 2.2.4
- 2.2.3
- 2.2.2
- 2.2.1
- 2.2.0
- 2.1.25
- 2.1.24
- 2.1.23
- 2.1.22
- 2.1.21
- 2.1.20
- 2.1.19
- 2.1.18
- 2.1.17
- 2.1.16
- 2.1.15
- 2.1.14
- 2.1.13
- 2.1.12
- v2.1.11
- v2.1.10
- v2.1.9
- v2.1.8
- v2.1.7
- v2.1.6
- v2.1.5
- v2.1.4
- v2.1.3
- v2.1.2
- v2.1.1
- v2.1.0
- v2.0.9
- v2.0.8
- v2.0.7
- v2.0.6
- v2.0.5
- v2.0.4
- v2.0.3
- v2.0.2
- v2.0.1
- v2.0.0
- v1.x-dev
- v1.3.3
- v1.3.2
- v1.3.1
- v1.3.0
- v1.2.1
- v1.2.0
- v1.1.16
- v1.1.15
- v1.1.14
- v1.1.13
- v1.1.12
- v1.1.11
- v1.1.10
- v1.1.9
- v1.1.8
- v1.1.7
- v1.1.6
- v1.1.5
- v1.1.4
- v1.1.3
- v1.1.2
- v1.1.1
This package is auto-updated.
Last update: 2026-09-28 16:22:51 UTC
README
File and media management for the yii2-skeleton admin: a folder tree of
uploaded files, on-demand image transformations (avif, webp, resized variants), and assets — a polymorphic link
between any record and a file, with a caption, alt text, link and viewport type. It depends on davidhirtz/yii2-skeleton
and on intervention/image with ext-imagick and ext-exif, behind Images\ImageProcessor. davidhirtz/yii2-cms gives entries and sections assets;
davidhirtz/yii2-media-video adds video files.
Installation
composer require davidhirtz/yii2-media ./yii migrate ./yii search/rebuild
The bundle bootstraps itself through extra.bootstrap (Hirtz\Media\Bootstrap): it registers the media module, the
admin/media submodule, the file and folder permissions, the file and transformation console commands, the @media
alias, the media message category, File and Folder on the search component, and a URL rule
<uploadPath>/<path:.*> → media/transformation/create that renders a transformation the first time its URL is requested.
The web server therefore has to fall back to index.php for a missing file under the upload path. Upgrading from 2.x:
see UPGRADE.md.
Configuration
modules.media
| Property | Default | Meaning |
|---|---|---|
allowedExtensions |
['gif', 'jpg', 'jpeg', 'png', 'svg'] |
extensions an upload may have (yii2-media-video appends mp4, webm, ogg unless set) |
assets |
[] |
Models\Asset subclasses, one per model that has assets |
autorotateImages |
true |
rewrite uploads upright by their EXIF orientation |
avifQuality |
60 |
AVIF quality of a transformation that sets none (yii2-anakin raises it to 80) |
baseUrl |
null |
URL prefix of the files; params.cdnUrl, then /<uploadPath> |
breakpoints |
xs 425, sm 768, md 1024, lg 1200, xl 1440 |
names Helpers\Size::breakpoint() accepts; a pixel width or a media query |
checkExtensionByMimeType |
false |
validate an upload by MIME type rather than extension |
defaultFolderOrder |
['position' => SORT_ASC] |
order of the folder list |
enableRenameFolders |
true |
allow a folder path to change (off for remote storage) |
enableDeleteNonEmptyFolders |
true |
allow deleting a folder with files |
folderCachedQueryDuration |
0 |
seconds the folder list is cached; false disables |
imageProcessor |
Images\ImageProcessor::class |
class name, configuration array or instance; getImageProcessor() creates it once |
jpegQuality |
75 |
JPEG quality of a transformation that sets none |
keepFilename |
true |
keep an upload's basename; false renames it to a random string |
maxFilesPerFolder |
false |
split uploads into numbered subdirectories of that size |
maxFolderRedirects |
1000 |
files a folder may hold for a rename to record a redirect per file; false never |
overwriteFiles |
false |
replace a file of the same name instead of numbering the upload |
resolution |
72 |
pixels per inch of a transformation that sets none |
transformableImageExtensions |
['jpg', 'jpeg', 'png'] |
extensions transformations are generated for |
transformationExtensions |
['avif', 'webp'] |
extra formats a transformation is offered in; one the server cannot encode is skipped (getTransformationExtensions(), listed under System › Server) |
transformations |
admin (120 wide), og (1200 × 630) |
Transformations\Transformation presets, see below |
uploadPath |
uploads (set by Bootstrap) |
directory under webroot and first URL segment |
webpQuality |
80 |
WebP quality of a transformation that sets none |
webroot |
@webroot |
file system root the upload path is relative to |
modules.admin.modules.media.cropRatios (?array, default null) replaces the aspect ratios the file crop offers.
params.cdnUrl sets baseUrl without touching the module config.
Transformations
A preset is a Transformations\Transformation with fluent setters — width(), height(), keepAspectRatio(), scaleUp()
(default false), backgroundColor(), backgroundAlpha(), jpegQuality(), webpQuality(), avifQuality(),
resolution(). The encoding setters fall back to the module's jpegQuality, webpQuality, avifQuality and
resolution, so a project sets its defaults once and a preset only names what differs; the geometry setters describe
the preset itself and have no module default:
use Hirtz\Media\Images\ImageProcessor; use Hirtz\Media\Transformations\Transformation; use Intervention\Image\Drivers\Vips\Driver as VipsDriver; 'modules' => [ 'media' => [ 'avifQuality' => 75, 'webpQuality' => 85, 'transformations' => [ Transformation::make('xs')->width(374), Transformation::make('hero')->width(1600)->height(400)->keepAspectRatio()->avifQuality(85), ], // another Intervention driver (here intervention/image-driver-vips), or a subclass of the processor 'imageProcessor' => [ 'class' => ImageProcessor::class, '__construct()' => [VipsDriver::class], ], ], ],
A type may also name a transformation that is not configured, as long as the name describes it: w_400, h_300,
w_200,h_300, w_400@2 (a modifier multiplies every dimension). Models\Interfaces\TransformationTypeInterface::transformations()
registers such a name, or an inline Transformation, on the module when the type definitions resolve. A name that neither
parses nor is configured throws. ./yii transformation/index lists what is registered and how many files carry each.
Container definitions
'container' => [ 'definitions' => [ Folder::class => ['types' => fn (): array => [/* Hirtz\Skeleton\Models\Types\Type objects */]], File::class => ['i18nAttributes' => ['name', 'alt_text']], EntryAsset::class => ['translatableAttributes' => ['alt_text']], ], ],
File::$customAttributes may declare project attributes stored in file.custom_attributes. An asset's content,
alt_text, link, embed_url, loading and fetchpriority are custom attributes already; all but loading and
fetchpriority are stored per language, which translatableAttributes on the asset subclass narrows (never
i18nAttributes). An asset declares no name: a project wanting one adds it in getDefaultCustomAttributes(). An asset
subclass ignores types; its types are the viewport types (AssetInterface::TYPE_VIEWPORT_MOBILE,
TYPE_VIEWPORT_DESKTOP) it declares itself.
Serving uploads
SVG is allowed by default and is not sanitised. Opened directly rather than through <img>, an SVG is a document of the
site's origin, and a script in it runs with the permissions of whoever opens it: an editor's upload reaching an admin.
Uploads are static files, so the web server has to send the headers that prevent it. The transformation route never
serves an SVG: an SVG is not transformable, so the web server answers with the file itself before the route is reached —
the headers below are the only protection.
location /uploads/ { add_header X-Content-Type-Options nosniff always; location ~* \.svg$ { add_header X-Content-Type-Options nosniff always; add_header Content-Security-Policy "script-src 'none'; sandbox" always; } }
<Directory "/path/to/web/uploads"> Header always set X-Content-Type-Options nosniff <FilesMatch "\.svg$"> Header always set Content-Security-Policy "script-src 'none'; sandbox" </FilesMatch> </Directory>
nginx does not inherit add_header into a block that declares its own, hence the repetition. A CDN in front of the
uploads (params.cdnUrl) needs the same two headers configured there.
Console commands
file/clear— deletes every file no asset referencesfile/orient— rewrites the images stored sideways by their EXIF orientation upright, correcting their width and height and deleting their transformationstransformation/index— lists every transformation name with its file count; a name no longer configured is shown in redtransformation/delete <name>— deletes the rows and directories of one transformation, so it is regenerated on demand
Giving a model assets
Five pieces: the interface and trait on the model, an asset_count column, an Asset subclass, and its registration.
use Hirtz\Media\Models\Interfaces\AssetModelInterface; use Hirtz\Media\Models\Traits\AssetModelTrait; use Hirtz\Skeleton\Models\Traits\AdminModelTrait; class Recipe extends ActiveRecord implements AssetModelInterface { use AdminModelTrait; use AssetModelTrait; final public const string AUTH_RECIPE = 'recipe'; public function getAssetClass(): string { return RecipeAsset::class; } public function allowsAssets(): bool { return $this->typeAllowsAssets(); } public function getAdminRoute(): array { return ['/admin/recipe/update', 'id' => $this->id]; } public function getPermissionName(): string { return static::AUTH_RECIPE; } }
use Hirtz\Media\Models\Asset; /** * @extends Asset<Recipe> */ class RecipeAsset extends Asset { public static function getModelClass(): string { return Recipe::class; } public static function getAdminControllerRoute(): string { return '/admin/recipe-asset'; } }
'modules' => ['media' => ['assets' => [RecipeAsset::class]]],
AssetModelTrait supplies getAssets(), updateAssetCount(), populateAssetRelations() and the type readers;
typeAllowsAssets() honours a type implementing Models\Interfaces\AssetModelTypeInterface (Models\Types\AssetModelType
or Models\Types\Traits\AssetModelTypeTrait on your own type) that declares allowAssets(false), sizes() or
transformations(). Asset::getPermissionName() answers the model's own permission, so the subclass declares nothing more.
The admin pages are the subclass's: a controller using Modules\Admin\Controllers\Traits\AssetControllerTrait with one
AccessControl rule over index, create, update, delete, delete-all, order, remove and status, plus index,
create and update views rendering Modules\Admin\Widgets\Grids\AssetGridView, FileGridView and
Widgets\Forms\AssetActiveForm under Widgets\Navs\AssetHeader. Hirtz\Cms\Modules\Admin\Controllers\EntryAssetController
and resources/views/admin/entry-asset/ in yii2-cms are the template. Register the subclass on the search component
so its captions are findable.
On the site, Widgets\Media::make()->asset($asset) renders an AVIF <img> carrying srcset, sizes, alt, loading
and fetchpriority. Asked for a <picture> (omitUnnecessaryPictureTag(false), a picture() closure or
extension(null)), it renders a source per transformation extension and an <img> in the file's own format.
Models\Queries\AssetQuery::withFiles() and Asset::populateModelRelations() load the files and the records of a mixed
list in one query per class.