├── .gitignore ├── LICENSE.md ├── README.md ├── composer.json ├── phpunit.xml ├── src ├── Attachment.php ├── Casts │ ├── AsAttachment.php │ └── AsAttachments.php └── Collection.php └── tests ├── Feature └── ExampleTest.php ├── Fixtures ├── ServiceProvider.php └── assets │ └── image.jpg ├── Pest.php ├── TestCase.php └── Unit └── CollectionTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | /.phpunit.cache 2 | /node_modules 3 | /public/build 4 | /public/hot 5 | /public/storage 6 | /storage/*.key 7 | /vendor 8 | .env 9 | .env.backup 10 | .env.production 11 | .phpactor.json 12 | .phpunit.result.cache 13 | Homestead.json 14 | Homestead.yaml 15 | auth.json 16 | npm-debug.log 17 | yarn-error.log 18 | /.fleet 19 | /.idea 20 | /.vscode 21 | composer.lock -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 NiftyCo, LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Attachments for Laravel 2 | 3 | > Turn any field on your Eloquent models into attachments 4 | 5 | > [!WARNING] 6 | > This package is not ready for general consumption 7 | 8 | ## Installation 9 | 10 | You can install the package via Composer: 11 | 12 | ```sh 13 | composer require aniftyco/laravel-attachments:dev-master 14 | ``` 15 | 16 | ## Usage 17 | 18 | ### Migrations 19 | 20 | Your migrations need to have a `Blueprint::jsonb()` column set on it. 21 | 22 | ```php 23 | return new class extends Migration { 24 | /** 25 | * Run the migrations. 26 | */ 27 | public function up(): void 28 | { 29 | Schema::create('users', function (Blueprint $table) { 30 | $table->id(); 31 | 32 | //... 33 | 34 | $table->jsonb('avatar')->nullable(); 35 | }); 36 | } 37 | }; 38 | ``` 39 | 40 | ### Adding Attachments to Models 41 | 42 | To add attachments to your Eloquent models, use the provided cast classes. 43 | 44 | #### Single Attachment 45 | 46 | Use the `AsAttachment` cast to handle a single attachment: 47 | 48 | ```php 49 | use NiftyCo\Attachments\Casts\AsAttachment; 50 | 51 | class User extends Model 52 | { 53 | protected function casts(): array 54 | { 55 | return [ 56 | 'avatar' => AsAttachment::class, 57 | ]; 58 | } 59 | } 60 | ``` 61 | 62 | To set an image as an attachment on your model: 63 | 64 | ```php 65 | use NiftyCo\Attachments\Attachment; 66 | 67 | class UserController 68 | { 69 | public function store(UserStoreRequest $request, User $user) 70 | { 71 | $user->avatar = Attachment::fromFile($request->file('avatar'), folder: 'avatars'); 72 | 73 | $user->save(); 74 | 75 | // ... 76 | } 77 | } 78 | ``` 79 | 80 | #### Multiple Attachments 81 | 82 | Use the `AsAttachments` cast to handle multiple attachments: 83 | 84 | ```php 85 | use NiftyCo\Attachments\Casts\AsAttachments; 86 | 87 | class Post extends Model 88 | { 89 | protected function casts(): array 90 | { 91 | return [ 92 | 'images' => AsAttachments::class, 93 | ]; 94 | } 95 | } 96 | ``` 97 | 98 | To attach multiple attachments to your model: 99 | 100 | ```php 101 | class PostController 102 | { 103 | public function store(PostStoreRequest $request, Post $post) 104 | { 105 | 106 | $images = $request->file('images'); 107 | 108 | // Loop over all images uploaded and add to the 109 | // collection of images already on the post 110 | array_map(function($image) use ($post) { 111 | $post->images->addFromFile($image); 112 | }, $images); 113 | 114 | // Save post 115 | $post->save(); 116 | 117 | // ... 118 | } 119 | } 120 | ``` 121 | 122 | ## Contributing 123 | 124 | Thank you for considering contributing to the Attachments for Laravel package! You can read the contribution guide [here](CONTRIBUTING.md). 125 | 126 | ## License 127 | 128 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 129 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aniftyco/laravel-attachments", 3 | "description": "Turn any field on your Eloquent models into attachments", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "NiftyCo, LLC", 8 | "homepage": "https://aniftyco.com" 9 | }, 10 | { 11 | "name": "Josh Manders", 12 | "homepage": "https://x.com/joshmanders" 13 | } 14 | ], 15 | "homepage": "https://github.com/aniftyco/laravel-attachments", 16 | "keywords": [ 17 | "Laravel", 18 | "Eloquent", 19 | "Attachments" 20 | ], 21 | "require": { 22 | "php": "^8.1", 23 | "illuminate/http": "^11.0|^12.0", 24 | "illuminate/database": "^11.0|^12.0" 25 | }, 26 | "autoload": { 27 | "psr-4": { 28 | "NiftyCo\\Attachments\\": "src/" 29 | } 30 | }, 31 | "require-dev": { 32 | "orchestra/testbench": "^9.5", 33 | "pestphp/pest": "^3.3", 34 | "illuminate/support": "^11.0" 35 | }, 36 | "autoload-dev": { 37 | "psr-4": { 38 | "Tests\\": "tests/", 39 | "Workbench\\App\\": "workbench/app/", 40 | "Workbench\\Database\\Factories\\": "workbench/database/factories/", 41 | "Workbench\\Database\\Seeders\\": "workbench/database/seeders/" 42 | } 43 | }, 44 | "scripts": { 45 | "post-autoload-dump": [ 46 | "@clear", 47 | "@prepare" 48 | ], 49 | "clear": "@php vendor/bin/testbench package:purge-skeleton --ansi", 50 | "prepare": "@php vendor/bin/testbench package:discover --ansi", 51 | "build": "@php vendor/bin/testbench workbench:build --ansi", 52 | "serve": [ 53 | "Composer\\Config::disableProcessTimeout", 54 | "@build", 55 | "@php vendor/bin/testbench serve --ansi" 56 | ], 57 | "lint": [ 58 | "@php vendor/bin/phpstan analyse --verbose --ansi" 59 | ], 60 | "test": "@php vendor/bin/pest" 61 | }, 62 | "extra": { 63 | "laravel": { 64 | "providers": [] 65 | } 66 | }, 67 | "config": { 68 | "allow-plugins": { 69 | "pestphp/pest-plugin": true 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | ./tests/Unit 10 | 11 | 12 | ./tests/Feature 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ./src 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/Attachment.php: -------------------------------------------------------------------------------- 1 | store($folder, $disk), 21 | size: $file->getSize(), 22 | extname: $file->extension(), 23 | mimeType: $file->getMimeType(), 24 | ); 25 | } 26 | 27 | public function __construct( 28 | private ?string $disk, 29 | private ?string $name, 30 | private ?int $size, 31 | private ?string $extname, 32 | private ?string $mimeType, 33 | ) { 34 | $this->url = Storage::disk($this->disk)->url($this->name); 35 | } 36 | 37 | public function url($full = false, $parameters = [], $secure = null): string 38 | { 39 | return $full ? url($this->url, $parameters, $secure) : $this->url; 40 | } 41 | 42 | public function toArray(): array 43 | { 44 | return [ 45 | 'disk' => $this->disk, 46 | 'name' => $this->name, 47 | 'size' => $this->size, 48 | 'extname' => $this->extname, 49 | 'mimeType' => $this->mimeType, 50 | ]; 51 | } 52 | 53 | public function jsonSerialize(): array 54 | { 55 | return $this->toArray(); 56 | } 57 | 58 | public function toJson($options = 0) 59 | { 60 | return json_encode($this->jsonSerialize(), $options); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Casts/AsAttachment.php: -------------------------------------------------------------------------------- 1 | toJson(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Casts/AsAttachments.php: -------------------------------------------------------------------------------- 1 | new Attachment(...(array) $item), $attachments)); 21 | } 22 | 23 | public function set(Model $model, string $key, mixed $attachments, array $attributes): ?string 24 | { 25 | if (!$attachments instanceof Collection) { 26 | return (new Collection([]))->toJson(); 27 | } 28 | 29 | return $attachments->toJson(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Collection.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class Collection extends BaseCollection 15 | { 16 | public function addFromFile(UploadedFile $uploadedFile, ?string $disk = null, ?string $folder = 'attachments'): static 17 | { 18 | $attachment = Attachment::fromFile($uploadedFile, $disk, $folder); 19 | 20 | return $this->add($attachment); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/Feature/ExampleTest.php: -------------------------------------------------------------------------------- 1 | get('/'); 5 | 6 | $response->assertStatus(200); 7 | }); 8 | -------------------------------------------------------------------------------- /tests/Fixtures/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | 'hello world'); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/Fixtures/assets/image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aniftyco/laravel-attachments/43337ebcc719367d2494481e441096e1b18b56d1/tests/Fixtures/assets/image.jpg -------------------------------------------------------------------------------- /tests/Pest.php: -------------------------------------------------------------------------------- 1 | extend(Tests\TestCase::class) 15 | // ->use(Illuminate\Foundation\Testing\RefreshDatabase::class) 16 | ->in('Feature'); 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Expectations 21 | |-------------------------------------------------------------------------- 22 | | 23 | | When you're writing tests, you often need to check that values meet certain conditions. The 24 | | "expect()" function gives you access to a set of "expectations" methods that you can use 25 | | to assert different things. Of course, you may extend the Expectation API at any time. 26 | | 27 | */ 28 | 29 | expect()->extend('toBeOne', function () { 30 | return $this->toBe(1); 31 | }); 32 | 33 | /* 34 | |-------------------------------------------------------------------------- 35 | | Functions 36 | |-------------------------------------------------------------------------- 37 | | 38 | | While Pest is very powerful out-of-the-box, you may have some testing code specific to your 39 | | project that you don't want to repeat in every file. Here you can also expose helpers as 40 | | global functions to help you to reduce the number of lines of code in your test files. 41 | | 42 | */ 43 | 44 | function something() 45 | { 46 | // .. 47 | } 48 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | toBeInstanceOf(IlluminateCollection::class); 10 | }); 11 | 12 | it('allows you to add an attachment from a file', function () { 13 | Storage::fake('public'); 14 | 15 | $collection = new Collection(); 16 | 17 | $collection->addFromFile(UploadedFile::fake()->image('image.jpg')); 18 | 19 | expect($collection->count())->toBe(1); 20 | expect($collection->first())->toBeInstanceOf(\NiftyCo\Attachments\Attachment::class); 21 | }); 22 | --------------------------------------------------------------------------------