Search by

davidhirtz / yii2-media

davidhirtz

Media management module for admin panel based on Yii 2.0 framework

Package info

github.com/davidhirtz/yii2-media

Homepage

Type:yii2-extension

pkg:composer/davidhirtz/yii2-media

Statistics

Installs: 1 519

Dependents: 5

Suggesters: 0

Stars: 1

Open Issues: 0

3.3.0 2026-09-28 16:20 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 references
  • file/orient — rewrites the images stored sideways by their EXIF orientation upright, correcting their width and height and deleting their transformations
  • transformation/index — lists every transformation name with its file count; a name no longer configured is shown in red
  • transformation/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.