diff --git a/config/eloquent-driver.php b/config/eloquent-driver.php index b6901f5a..7465d975 100644 --- a/config/eloquent-driver.php +++ b/config/eloquent-driver.php @@ -14,6 +14,7 @@ 'driver' => 'file', 'model' => \Statamic\Eloquent\Assets\AssetModel::class, 'asset' => \Statamic\Eloquent\Assets\Asset::class, + 'use_model_keys_for_ids' => false, ], 'blueprints' => [ diff --git a/src/Assets/Asset.php b/src/Assets/Asset.php index 5d917cab..ca2aa540 100644 --- a/src/Assets/Asset.php +++ b/src/Assets/Asset.php @@ -30,12 +30,15 @@ public function syncOriginal() protected $existsOnDisk = false; protected $removedData = []; + protected $model; + public static function fromModel(Model $model) { return (new static()) ->container($model->container) ->path(Str::replace('//', '/', $model->folder.'/'.$model->basename)) ->hydrateMeta($model->meta) + ->model($model) ->syncOriginal(); } @@ -60,6 +63,20 @@ public function meta($key = null) return $meta; } + if (config('statamic.eloquent-driver.assets.use_model_keys_for_ids', false)) { + if ($this->model) { + return $this->model->meta; + } + + $meta = $this->generateMeta(); + + if (! $meta['data']) { + $meta['data'] = []; + } + + return $meta; + } + return Blink::once($this->metaCacheKey(), function () { if ($model = app('statamic.eloquent.assets.model')::where([ 'container' => $this->containerHandle(), @@ -129,7 +146,11 @@ public function writeMeta($meta) { $meta['data'] = Arr::removeNullValues($meta['data']); - self::makeModelFromContract($this, $meta)?->save(); + if ($model = self::makeModelFromContract($this, $meta)) { + $model->save(); + + $this->model = $model; + } Blink::put('eloquent-asset-meta-exists-'.$this->id(), true); } @@ -147,11 +168,21 @@ public static function makeModelFromContract(AssetContract $source, $meta = []) $original = $source->getOriginal(); - $model = app('statamic.eloquent.assets.model')::firstOrNew([ - 'container' => $source->containerHandle(), - 'folder' => Arr::get($original, 'folder', $source->folder()), - 'basename' => Arr::get($original, 'basename', $source->basename()), - ])->fill([ + $model = false; + + if (config('statamic.eloquent-driver.assets.use_model_keys_for_ids', false)) { + $model = $source->model(); + } + + if (! $model) { + $model = app('statamic.eloquent.assets.model')::firstOrNew([ + 'container' => $source->containerHandle(), + 'folder' => Arr::get($original, 'folder', $source->folder()), + 'basename' => Arr::get($original, 'basename', $source->basename()), + ]); + } + + $model->fill([ 'meta' => $meta, 'filename' => $source->filename(), 'extension' => $extension, @@ -195,7 +226,8 @@ public function move($folder, $filename = null) $this->path($newPath); $this->save(); - if ($oldMetaPath != $this->metaPath()) { + // if we arent referencing assets by the database model id, then we need to find any old models by the previous path and update them + if (! config('statamic.eloquent-driver.assets.use_model_keys_for_ids', false) && ($oldMetaPath != $this->metaPath())) { $oldMetaModel = app('statamic.eloquent.assets.model')::where([ 'container' => $this->containerHandle(), 'folder' => $oldFolder, @@ -213,6 +245,30 @@ public function move($folder, $filename = null) return $this; } + public function model($model = null) + { + if (func_num_args() === 0) { + return $this->model; + } + + $this->model = $model; + + return $this; + } + + public function id($id = null) + { + if ($id || ! config('statamic.eloquent-driver.assets.use_model_keys_for_ids', false)) { + return parent::id($id); + } + + if (! $this->model) { + throw new \Exception('ID is not available until asset is saved'); + } + + return $this->model->getKey(); + } + public function getCurrentDirtyStateAttributes(): array { return array_merge([ diff --git a/src/Assets/AssetRepository.php b/src/Assets/AssetRepository.php index 9b2da69f..c28e5535 100644 --- a/src/Assets/AssetRepository.php +++ b/src/Assets/AssetRepository.php @@ -13,12 +13,25 @@ class AssetRepository extends BaseRepository { public function findById($id): ?AssetContract { + $blinkKey = "eloquent-asset-{$id}"; + [$container, $path] = explode('::', $id); + if (config('statamic.eloquent-driver.assets.use_model_keys_for_ids', false) && (str_contains($path, '.') === false)) { + $item = Blink::once($blinkKey, fn () => $this->query()->where('id', $path)->first()); + + if (! $item) { + Blink::forget($blinkKey); + + return null; + } + + return $item; + } + $filename = Str::afterLast($path, '/'); $folder = str_contains($path, '/') ? Str::beforeLast($path, '/') : '/'; - $blinkKey = "eloquent-asset-{$id}"; $item = Blink::once($blinkKey, function () use ($container, $filename, $folder) { return $this->query() ->where('container', $container) @@ -38,6 +51,11 @@ public function findById($id): ?AssetContract public function findByUrl(string $url) { + // handle find('model-key'), with no container + if (! str_contains('.', $url)) { + return $this->findById('::'.$url); + } + if (! $container = $this->resolveContainerFromUrl($url)) { return null; } diff --git a/src/Commands/UpdateAssetReferencesToUseModelKeys.php b/src/Commands/UpdateAssetReferencesToUseModelKeys.php new file mode 100644 index 00000000..6685130f --- /dev/null +++ b/src/Commands/UpdateAssetReferencesToUseModelKeys.php @@ -0,0 +1,51 @@ +reject(fn ($container) => $this->option('container') != 'all' && $this->option('container') != $container->handle()) + ->each(fn ($container) => $this->processContainer($container)); + + $this->info('Complete'); + } + + private function processContainer(AssetContainer $container) + { + $this->info("Container: {$container->handle()}"); + + $container->queryAssets()->get()->each(function ($item) use ($container) { + return AssetReferenceUpdater::item($item) + ->filterByContainer($container) + ->updateReferences($item->path(), $item->id()); + }); + } +} diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index dbc4a73c..297471b4 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -39,6 +39,7 @@ use Statamic\Eloquent\Taxonomies\TermQueryBuilder; use Statamic\Eloquent\Taxonomies\TermRepository; use Statamic\Eloquent\Tokens\TokenRepository; +use Statamic\Listeners\UpdateAssetReferences; use Statamic\Providers\AddonServiceProvider; use Statamic\Statamic; @@ -93,6 +94,7 @@ public function boot() Commands\ImportRevisions::class, Commands\ImportTaxonomies::class, Commands\SyncAssets::class, + Commands\UpdateAssetReferencesToUseModelKeys::class, ]); $this->addAboutCommandInfo(); @@ -256,6 +258,11 @@ private function registerAssets() }); Statamic::repository(AssetRepositoryContract::class, AssetRepository::class); + + // we dont need asset references to run on asset save if we are linking by id, as the references dont update + if (config('statamic.eloquent-driver.assets.use_model_keys_for_ids', false)) { + UpdateAssetReferences::disable(); + } } private function registerBlueprints() diff --git a/tests/Assets/AssetTest.php b/tests/Assets/AssetTest.php index 6f763cc1..0dbcecaf 100644 --- a/tests/Assets/AssetTest.php +++ b/tests/Assets/AssetTest.php @@ -4,7 +4,9 @@ use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Storage; +use Orchestra\Testbench\Attributes\DefineEnvironment; use PHPUnit\Framework\Attributes\Test; +use Statamic\Eloquent\Assets\Asset; use Statamic\Facades; use Tests\TestCase; @@ -52,4 +54,53 @@ public function saving_an_asset_clears_the_eloquent_blink_cache() $this->assertFalse(Facades\Blink::has("eloquent-asset-{$asset->id()}")); } + + #[Test] + public function not_referencing_by_id_gives_a_container_and_path_id() + { + $asset = Facades\Asset::find('test::f.jpg'); + + $this->assertNotSame($asset->id(), $asset->model()->getKey()); + $this->assertStringContainsString('::', $asset->id()); + } + + #[Test] + #[DefineEnvironment('setUseModelKeysConfig')] + public function referencing_by_id_gives_a_model_id() + { + $asset = Facades\Asset::find('test::f.jpg'); + + $this->assertSame($asset->id(), $asset->model()->getKey()); + } + + #[Test] + #[DefineEnvironment('setUseModelKeysConfig')] + public function an_error_is_thrown_when_getting_id_before_asset_is_saved() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('ID is not available until asset is saved'); + + Storage::disk('test')->put('new.jpg', ''); + Facades\Asset::make()->container('test')->path('new.jpg')->id(); + } + + #[Test] + #[DefineEnvironment('setUseModelKeysConfig')] + public function using_find_with_an_id_returns_an_asset() + { + $asset = Facades\Asset::find('test::6'); + + $this->assertInstanceOf(Asset::class, $asset); + $this->assertSame('f.jpg', $asset->basename()); + + $asset = Facades\Asset::find('6'); + + $this->assertInstanceOf(Asset::class, $asset); + $this->assertSame('f.jpg', $asset->basename()); + } + + protected function setUseModelKeysConfig($app) + { + $app['config']->set('statamic.eloquent-driver.assets.use_model_keys_for_ids', true); + } }