{"id":51433203,"url":"https://github.com/oi-lab/oi-laravel-attachments","last_synced_at":"2026-07-05T05:03:55.903Z","repository":{"id":364800007,"uuid":"1269195067","full_name":"oi-lab/oi-laravel-attachments","owner":"oi-lab","description":"A Laravel package for polymorphic file attachments. Attach files to any Eloquent model, organize them into named collections with ordering, store rich file metadata (EXIF, IPTC, dimensions, color profile), and group files into nested folders.","archived":false,"fork":false,"pushed_at":"2026-06-29T19:40:28.000Z","size":98,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-29T21:24:46.331Z","etag":null,"topics":["attachment","laravel","package","polymorphic"],"latest_commit_sha":null,"homepage":"https://dev.olacombe.com/documentation/oi-laravel-attachments/getting-started","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/oi-lab.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-14T12:20:14.000Z","updated_at":"2026-06-29T19:40:31.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/oi-lab/oi-laravel-attachments","commit_stats":null,"previous_names":["oi-lab/oi-laravel-attachments"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/oi-lab/oi-laravel-attachments","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oi-lab%2Foi-laravel-attachments","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oi-lab%2Foi-laravel-attachments/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oi-lab%2Foi-laravel-attachments/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oi-lab%2Foi-laravel-attachments/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/oi-lab","download_url":"https://codeload.github.com/oi-lab/oi-laravel-attachments/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oi-lab%2Foi-laravel-attachments/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35143827,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-05T02:00:06.290Z","response_time":100,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["attachment","laravel","package","polymorphic"],"created_at":"2026-07-05T05:03:55.256Z","updated_at":"2026-07-05T05:03:55.893Z","avatar_url":"https://github.com/oi-lab.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cimg src=\"./assets/github-preview.png\" alt=\"OI Laravel TypeScript Generator\" width=\"100%\" /\u003e\n\n# OI Laravel Attachments BETA\n\n[![Latest Version on Packagist](https://img.shields.io/packagist/v/oi-lab/oi-laravel-attachments.svg)](https://packagist.org/packages/oi-lab/oi-laravel-attachments)\n[![Total Downloads](https://img.shields.io/packagist/dt/oi-lab/oi-laravel-attachments.svg)](https://packagist.org/packages/oi-lab/oi-laravel-attachments)\n[![Tests](https://img.shields.io/github/actions/workflow/status/oi-lab/oi-laravel-attachments/tests.yml?label=tests)](https://github.com/oi-lab/oi-laravel-attachments/actions)\n[![License](https://img.shields.io/github/license/oi-lab/oi-laravel-attachments)](LICENSE)\n\nA Laravel package for polymorphic file attachments. Attach files to **any** Eloquent model, organize them into named collections with ordering, store rich file metadata (EXIF, IPTC, dimensions, color profile), and group files into nested folders.\n\n## Features\n\n- **Polymorphic Attachments**: Attach files to any model via a single `HasAttachments` trait\n- **Named Collections**: Group attachments per model (e.g. `gallery`, `cover`, `documents`)\n- **Ordering**: First-class sort support with reorder, move, and swap helpers\n- **Rich File Metadata**: `File` model captures mimetype, filesize, dimensions, MD5, EXIF, IPTC, and color info\n- **Folders**: Optional self-nesting folder tree for organizing files\n- **Upload Actions**: Single-call actions to store uploads and attach them to a model\n- **Audit Tracking**: Automatic `created_by` / `updated_by` tracking on every record\n- **Configurable Models**: Swap in your own `File`, `Folder`, or `Attachment` subclasses\n- **Storage Agnostic**: Works with any Flysystem disk (local, S3, etc.)\n\n## How It Works\n\nThe package revolves around three models:\n\n- **File** — a stored file and its metadata, optionally living inside a `Folder`.\n- **Folder** — a self-nesting container (`parent_id` tree) for organizing files.\n- **Attachment** — a polymorphic pivot linking a `File` to any `attachable` model, with a `collection` name and `sort` order.\n\nHost models opt in with the `HasAttachments` trait, which exposes the full attach / detach / sync / reorder API. All package internals resolve model classes through the `OiLaravelAttachments` resolver, so you can override any model from config without touching package code.\n\n## Requirements\n\n- PHP 8.2+\n- Laravel 11.0+, 12.0+, or 13.0+\n\n## Installation\n\n```bash\ncomposer require oi-lab/oi-laravel-attachments\n```\n\nThe package auto-discovers and registers its service provider — no manual registration required.\n\n### Local Development\n\nFor local development, add this to your main project's `composer.json`:\n\n```json\n{\n    \"repositories\": [\n        {\n            \"type\": \"path\",\n            \"url\": \"./packages/oi-lab/oi-laravel-attachments\"\n        }\n    ]\n}\n```\n\nThen:\n\n```bash\ncomposer require oi-lab/oi-laravel-attachments\n```\n\n### Publish \u0026 Migrate\n\nPublish the migrations (and optionally the config) and run them:\n\n```bash\nphp artisan vendor:publish --tag=oi-laravel-attachments-migrations\nphp artisan vendor:publish --tag=oi-laravel-attachments-config\nphp artisan migrate\n```\n\nThis creates the `folders`, `files`, and `attachments` tables.\n\n## Configuration\n\nThe config file `config/oi-laravel-attachments.php` exposes the following options:\n\n```php\nreturn [\n    // Model used for the created_by / updated_by audit relationships\n    'user_model' =\u003e 'App\\Models\\User',\n\n    // Model classes used by the package — override with your own subclasses\n    'models' =\u003e [\n        'file' =\u003e OiLab\\OiLaravelAttachments\\Models\\File::class,\n        'folder' =\u003e OiLab\\OiLaravelAttachments\\Models\\Folder::class,\n        'attachment' =\u003e OiLab\\OiLaravelAttachments\\Models\\Attachment::class,\n    ],\n\n    // Disk used to store uploaded files (defaults to ATTACHMENTS_DISK, then FILESYSTEM_DISK)\n    'disk' =\u003e env('ATTACHMENTS_DISK', env('FILESYSTEM_DISK', 'local')),\n\n    // Directory uploaded files are stored under\n    'directory' =\u003e 'uploads',\n];\n```\n\n## Usage\n\n### Make a Model Attachable\n\nAdd the `HasAttachments` trait to any model:\n\n```php\nuse Illuminate\\Database\\Eloquent\\Model;\nuse OiLab\\OiLaravelAttachments\\Concerns\\HasAttachments;\n\nclass Product extends Model\n{\n    use HasAttachments;\n}\n```\n\n### Attach \u0026 Detach Files\n\n```php\n// Attach a File (model instance or id) to a collection\n$product-\u003eattachFile($file, collection: 'gallery');\n\n// Detach\n$product-\u003edetachFile($file, 'gallery');\n\n// Read attachments (ordered by sort)\n$product-\u003eattachments;                    // all attachments\n$product-\u003eattachments('gallery')-\u003eget();  // one collection\n$product-\u003esingleAttachment('cover');      // MorphOne for single-file collections\n\n// Read the underlying File models directly\n$product-\u003eattached_files;                 // Collection\u003cFile\u003e\n```\n\n### Sync a Collection\n\n`syncAttachments()` replaces every attachment in a collection. `syncAttachmentsIfChanged()` does the same but skips the database work when the ids and their order already match:\n\n```php\n$product-\u003esyncAttachments([$id1, $id2, $id3], 'gallery');\n\n// Returns false and does nothing if the collection is already in this exact state\n$changed = $product-\u003esyncAttachmentsIfChanged([$id1, $id2, $id3], 'gallery');\n```\n\n### Reorder\n\n```php\n// Map file ids to their new sort position\n$product-\u003ereorderAttachments([\n    $fileA-\u003eid =\u003e 0,\n    $fileB-\u003eid =\u003e 1,\n    $fileC-\u003eid =\u003e 2,\n], 'gallery');\n```\n\n### Uploading Files\n\nUse the action classes instead of building `File` records manually. `StoreUploadedFile` persists the upload and captures its metadata; `AttachUploadedFiles` stores many uploads and attaches them in a single call:\n\n```php\nuse OiLab\\OiLaravelAttachments\\Actions\\StoreUploadedFile;\nuse OiLab\\OiLaravelAttachments\\Actions\\AttachUploadedFiles;\n\npublic function store(Request $request, Product $product): RedirectResponse\n{\n    // Store a single upload, get back a File model\n    $file = StoreUploadedFile::handle($request-\u003efile('document'));\n\n    // Store multiple uploads and attach them to the product\n    AttachUploadedFiles::handle($product, $request-\u003efile('images'), 'gallery');\n\n    return back();\n}\n```\n\n### Working with Files\n\nThe `File` model offers type helpers, storage access, and a search scope:\n\n```php\n$file-\u003eisImage();   // mimetype starts with image/\n$file-\u003eisVideo();\n$file-\u003eisAudio();\n\n$file-\u003egetFullPath();  // absolute path (local disks only)\n$file-\u003egetStream();    // stream resource (works with S3 etc.)\n\nFile::search('invoice')-\u003eget(); // matches filename, title, or description\n```\n\nFile metadata is exposed as a value object rather than raw JSON:\n\n```php\n$file-\u003emetadata-\u003ewidth;\n$file-\u003emetadata-\u003eheight;\n$file-\u003emetadata-\u003easpect_ratio;\n$file-\u003emetadata-\u003eexif;        // ExifValueObject|null\n$file-\u003emetadata-\u003eiptc;        // IptcValueObject|null\n$file-\u003emetadata-\u003eresolution;  // ResolutionValueObject|null\n```\n\n### Folders\n\nFiles can optionally be organized into a nested folder tree:\n\n```php\n$folder = Folder::create(['name' =\u003e 'Invoices']);\n$child = Folder::create(['name' =\u003e '2026', 'parent_id' =\u003e $folder-\u003eid]);\n\n$folder-\u003echildren;  // HasMany\u003cFolder\u003e\n$folder-\u003efiles;     // HasMany\u003cFile\u003e\n$child-\u003eparent;     // BelongsTo\u003cFolder\u003e\n```\n\n## Customizing Models\n\nTo extend a model, subclass the package model and point the config at your class. Always resolve model classes through the `OiLaravelAttachments` helper so your override is respected everywhere:\n\n```php\nuse OiLab\\OiLaravelAttachments\\Models\\File as BaseFile;\n\nclass File extends BaseFile\n{\n    // your customizations\n}\n```\n\n```php\n// config/oi-laravel-attachments.php\n'models' =\u003e [\n    'file' =\u003e App\\Models\\File::class,\n],\n```\n\n## Events\n\nEach action dispatches a Laravel event you can listen to. All events live in `OiLab\\OiLaravelAttachments\\Events`:\n\n| Event | Dispatched when |\n|-------|-----------------|\n| `FileStored` | An uploaded file is stored by `StoreUploadedFile` |\n| `FileAttached` | A file is attached to a model |\n| `FileDetached` | One or more attachments are removed |\n| `AttachmentsSynced` | A collection is replaced via `syncAttachments()` |\n| `AttachmentsReordered` | Attachments are reordered |\n| `AttachmentCreated` / `AttachmentUpdated` / `AttachmentDeleted` | Model-level attachment lifecycle |\n| `FileCreated` / `FileUpdated` / `FileDeleted` | A file record is created, updated, or soft deleted |\n| `FileMoved` | A file is moved to a different folder |\n| `FileRestored` | A soft-deleted file is restored |\n| `FolderCreated` / `FolderUpdated` / `FolderDeleted` | A folder is created, updated, or soft deleted |\n| `FolderMoved` | A folder is moved to a different parent |\n| `FolderRestored` | A soft-deleted folder is restored |\n\n```php\nuse Illuminate\\Support\\Facades\\Event;\nuse OiLab\\OiLaravelAttachments\\Events\\FileStored;\n\nEvent::listen(function (FileStored $event) {\n    if ($event-\u003efile-\u003eisImage()) {\n        // generate thumbnails, optimize, ...\n    }\n});\n```\n\nSee the [Events documentation](docs/advanced/events.md) for each event's payload and dispatch behaviour.\n\n## Database Schema\n\n| Table | Purpose |\n|-------|---------|\n| `folders` | Self-nesting folder tree (`parent_id`), soft-deletable |\n| `files` | Stored files with metadata, optional `folder_id`, soft-deletable |\n| `attachments` | Polymorphic pivot (`attachable`, `file_id`, `collection`, `sort`) |\n\nAll three tables carry a unique `uuid`, `created_by` / `updated_by` audit columns, and timestamps.\n\n## Testing\n\nThis package ships **75 tests** covering the attachment trait, models and relationships, sorting, upload actions, file metadata, audit tracking, and events.\n\n```bash\n# Run all tests\nvendor/bin/pest\n\n# Run a specific suite\nvendor/bin/pest tests/Unit\nvendor/bin/pest tests/Feature\n\n# With coverage\nvendor/bin/pest --coverage\n```\n\n## AI Assistant Skills\n\nThis package ships a skill file that gives AI coding assistants (Claude Code, JetBrains Junie) context about how attachments work. Install it into your application with the Artisan command:\n\n```bash\nphp artisan oi:skills\n```\n\nThis writes `.claude/skills/oilab-laravel-attachments/SKILL.md` and `.junie/skills/oilab-laravel-attachments/SKILL.md`, and adds an `oi-lab/oi-laravel-attachments` rules section to your project's `CLAUDE.md`. See the [documentation](docs/advanced/skills.md) for details.\n\n## Contributing\n\nContributions are welcome! When contributing:\n\n1. Write tests for new features\n2. Ensure all tests pass: `vendor/bin/pest`\n3. Run `vendor/bin/pint` to match the project code style\n4. Update documentation as needed\n\n## License\n\nThis package is open-source software licensed under the [MIT license](LICENSE).\n\n## Credits\n\n**[Olivier Lacombe](https://www.olacombe.com)** - Creator and maintainer\n\nOlivier is a Product \u0026 Technology Director based in Montpellier, France, with over 20 years of experience innovating in UX/UI and emerging technologies. He specializes in guiding enterprises toward cutting-edge digital solutions, combining user-centered design with continuous optimization and artificial intelligence integration.\n\n**Projects \u0026 Resources:**\n- [OI Dev Docs](https://dev.olacombe.com) - Documentation for all Open Source OI Lab packages\n- [OnAI](https://onai.olacombe.com) - Training courses and masterclasses on generative AI for businesses\n- [Promptr](https://promptr.olacombe.com) - Prompt engineering Management Platform\n\n## Support\n\nFor support, please open an issue on the [GitHub repository](https://github.com/oi-lab/oi-laravel-attachments/issues).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Foi-lab%2Foi-laravel-attachments","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Foi-lab%2Foi-laravel-attachments","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Foi-lab%2Foi-laravel-attachments/lists"}