Search by

rasuvaeff / yii3-filestorage-attachments

rasuvaeff

Tenant-scoped polymorphic file attachments for rasuvaeff/yii3-filestorage

Package info

github.com/rasuvaeff/yii3-filestorage-attachments

pkg:composer/rasuvaeff/yii3-filestorage-attachments

Statistics

Installs: 835

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.2.0 2026-10-28 19:13 UTC

This package is auto-updated.

Last update: 2026-11-01 19:40:45 UTC


README

rasuvaeff/yii3-filestorage-attachments provides a small database-backed owner-to-file link service for Yii3 Filestorage. It covers the common polymorphic attachment case without requiring Active Record.

Install

composer require rasuvaeff/yii3-filestorage-attachments

The package requires rasuvaeff/yii3-filestorage-db. Run its file-table migration first, then register this package's migration namespace:

MigrationService::class => [
    'setSourceNamespaces()' => [[
        'Rasuvaeff\\Yii3FilestorageDb\\Migration',
        'Rasuvaeff\\Yii3FilestorageAttachments\\Migration',
    ]],
],

The config plugin supplies AttachmentTableName and Attachments. It does not bind FileScopeProviderInterface; applications with tenants bind that contract once, and the same provider is used by yii3-filestorage-db.

Usage

$attachments->attach(
    file: $file,
    ownerType: 'order',
    ownerId: (string) $orderId,
    role: 'invoice',
);

$invoices = $attachments->forOwner(
    ownerType: 'order',
    ownerId: (string) $orderId,
    role: 'invoice',
);

$attachments->detach(
    file: $file,
    ownerType: 'order',
    ownerId: (string) $orderId,
    role: 'invoice',
);

attach() is idempotent and returns false when the exact link already exists. forOwner() returns list<Attachment> in insertion order; omit role to list every role. detachOwner() removes every role and file link for an owner and must be called by the application or an Active Record lifecycle when that owner is deleted.

"Exactly one invoice per order" is sync() — replacing a role's file set in one transaction, so a crash cannot leave two invoices where one was meant:

$result = $attachments->sync(
    files: [$newInvoice],
    ownerType: 'order',
    ownerId: (string) $orderId,
    role: 'invoice',
);
// ['added' => 1, 'removed' => 1]

Links whose file is not in the list are removed, missing ones added — both inside one database transaction. Surviving links keep their insertion order (the list's order is not a sort order; there is deliberately no sort column), and every file must be visible in the current scope before anything is written.

Concurrency: the transaction's delete locks the role's rows until commit, so syncs of a role that already holds anything serialize correctly. Two concurrent syncs against a role that is empty on both sides may both commit their sets (the unique key still prevents duplicate links) — serialize such callers if that case matters.

The table has a real foreign key to filestorage_file, so deleting a file row automatically removes its links. There is deliberately no generic foreign key to owner tables.

SQLite prerequisite: SQLite enforces ON DELETE CASCADE only when every connection enables PRAGMA foreign_keys = ON. The package does not configure connections injected into it — do this in your application's connection setup. MySQL/MariaDB and PostgreSQL enforce the cascade by default.

Table names and scope

Set rasuvaeff/yii3-filestorage-attachments.attachmentTable and tablePrefix in application params when the defaults conflict. The migration and runtime service resolve the same typed AttachmentTableName.

With no scope provider, links use an internal empty scope key. With a bound FileScopeProviderInterface, every operation is restricted to its current scope and a file must be visible in that scope before it can be attached.

Verification

make build
make test-integration
make rector

The integration suite applies the real yii3-filestorage-db and attachments migrations on SQLite and exercises the documented migration path.