diff --git a/src/Contracts/Entries/EntryRepository.php b/src/Contracts/Entries/EntryRepository.php index c306eba1a5..d34a7b0120 100644 --- a/src/Contracts/Entries/EntryRepository.php +++ b/src/Contracts/Entries/EntryRepository.php @@ -2,6 +2,8 @@ namespace Statamic\Contracts\Entries; +use Closure; + interface EntryRepository { public function all(); @@ -16,6 +18,16 @@ public function findOrFail($id); public function findByUri(string $uri); + public function findOrNew($id); + + public function findOr($id, Closure $callback); + + public function firstOrNew(array $attributes, array $values = []); + + public function firstOrCreate(array $attributes, array $values = []); + + public function updateOrCreate(array $attributes, array $values = []); + public function make(); public function query(); diff --git a/src/Contracts/Taxonomies/TermRepository.php b/src/Contracts/Taxonomies/TermRepository.php index df0c4919b0..cd445a343e 100644 --- a/src/Contracts/Taxonomies/TermRepository.php +++ b/src/Contracts/Taxonomies/TermRepository.php @@ -2,6 +2,8 @@ namespace Statamic\Contracts\Taxonomies; +use Closure; + interface TermRepository { public function all(); @@ -16,6 +18,16 @@ public function findByUri(string $uri); public function findOrFail($id); + public function findOrNew($id); + + public function findOr($id, Closure $callback); + + public function firstOrNew(array $attributes, array $values = []); + + public function firstOrCreate(array $attributes, array $values = []); + + public function updateOrCreate(array $attributes, array $values = []); + public function make(?string $slug = null); public function query(); diff --git a/src/Facades/Entry.php b/src/Facades/Entry.php index c5d05e54b1..60873d47bb 100644 --- a/src/Facades/Entry.php +++ b/src/Facades/Entry.php @@ -13,6 +13,9 @@ * @method static \Statamic\Contracts\Entries\Entry findOrFail($id) * @method static null|\Statamic\Contracts\Entries\Entry findByUri(string $uri, string $site) * @method static \Statamic\Contracts\Entries\Entry make() + * @method static \Statamic\Contracts\Entries\Entry firstOrNew(array $attributes, array $values) + * @method static \Statamic\Contracts\Entries\Entry firstOrCreate(array $attributes, array $values) + * @method static \Statamic\Contracts\Entries\Entry updateOrCreate(array $attributes, array $values) * @method static \Statamic\Contracts\Entries\QueryBuilder query() * @method static void save($entry) * @method static void delete($entry) diff --git a/src/Facades/Term.php b/src/Facades/Term.php index 11d366bb83..ab44b46805 100644 --- a/src/Facades/Term.php +++ b/src/Facades/Term.php @@ -19,6 +19,9 @@ * @method static save($term) * @method static delete($term) * @method static TermQueryBuilder query() + * @method static TermContract firstOrNew(array $attributes, array $values) + * @method static TermContract firstOrCreate(array $attributes, array $values) + * @method static TermContract updateOrCreate(array $attributes, array $values) * @method static int entriesCount(Term $term) * @method static void substitute($item) * @method static \Illuminate\Support\Collection applySubstitutions($items) diff --git a/src/Stache/Query/EntryQueryBuilder.php b/src/Stache/Query/EntryQueryBuilder.php index d20fe0c77b..908e18dd31 100644 --- a/src/Stache/Query/EntryQueryBuilder.php +++ b/src/Stache/Query/EntryQueryBuilder.php @@ -2,11 +2,13 @@ namespace Statamic\Stache\Query; +use Closure; use Statamic\Contracts\Entries\QueryBuilder; use Statamic\Entries\EntryCollection; use Statamic\Facades; use Statamic\Facades\Blink; use Statamic\Facades\Collection; +use Statamic\Facades\Entry; use Statamic\Support\Arr; class EntryQueryBuilder extends Builder implements QueryBuilder @@ -200,4 +202,70 @@ public function prepareForFakeQuery(): array return $data; } + + public function findOrNew($id) + { + if (! is_null($entry = $this->find($id))) { + return $entry; + } + + return Entry::make(); + } + + public function findOr($id, Closure $callback) + { + if (! is_null($entry = $this->find($id))) { + return $entry; + } + + return $callback(); + } + + public function firstOrNew(array $attributes = [], array $values = []) + { + if (! is_null($instance = $this->where($attributes)->first())) { + return $instance; + } + + $data = array_merge($attributes, $values); + + if (($this->collections && count($this->collections) > 1) && ! isset($data['collection'])) { + throw new \Exception('Please specify a collection.'); + } + + return Entry::make() + ->collection($this->collections[0] ?? $data['collection']) + ->slug($data['slug'] ?? (isset($data['title']) ? Str::slug($data['title']) : null)) + ->data(Arr::except($data, ['collection', 'slug'])); + } + + public function firstOrCreate(array $attributes = [], array $values = []) + { + $entry = $this->firstOrNew($attributes, $values); + + // When the entry is dirty, it means it's new and should be saved. + if ($entry->isDirty()) { + $entry->save(); + } + + return $entry; + } + + public function updateOrCreate(array $attributes, array $values = []) + { + $entry = $this->firstOrNew($attributes, $values); + + // When the entry is clean, it means it's existing and should be updated. + if ($entry->isClean()) { + if ($slug = Arr::get($values, 'slug')) { + $entry->slug($slug); + } + + $entry->merge(Arr::except($values, ['slug'])); + } + + $entry->save(); + + return $entry; + } } diff --git a/src/Stache/Query/TermQueryBuilder.php b/src/Stache/Query/TermQueryBuilder.php index deda55b377..e674d75af4 100644 --- a/src/Stache/Query/TermQueryBuilder.php +++ b/src/Stache/Query/TermQueryBuilder.php @@ -2,9 +2,13 @@ namespace Statamic\Stache\Query; +use Closure; use Statamic\Facades; use Statamic\Facades\Collection; +use Statamic\Facades\Term; use Statamic\Support\Arr; +use Statamic\Support\Str; +use Statamic\Taxonomies\LocalizedTerm; use Statamic\Taxonomies\TermCollection; class TermQueryBuilder extends Builder @@ -202,4 +206,70 @@ public function prepareForFakeQuery(): array return $data; } + + public function findOrNew($id) + { + if (! is_null($term = $this->find($id))) { + return $term; + } + + return Term::make(); + } + + public function findOr($id, Closure $callback) + { + if (! is_null($term = $this->find($id))) { + return $term; + } + + return $callback(); + } + + public function firstOrNew(array $attributes = [], array $values = []) + { + if (! is_null($instance = $this->where($attributes)->first())) { + return $instance; + } + + $data = array_merge($attributes, $values); + + if (($this->taxonomies && count($this->taxonomies) > 1) && ! isset($data['taxonomy'])) { + throw new \Exception('Please specify a taxonomy.'); + } + + return Term::make() + ->taxonomy($this->taxonomies[0] ?? $data['taxonomy']) + ->slug($data['slug'] ?? (isset($data['title']) ? Str::slug($data['title']) : null)) + ->data(Arr::except($data, ['taxonomy', 'slug'])); + } + + public function firstOrCreate(array $attributes = [], array $values = []) + { + $term = $this->firstOrNew($attributes, $values); + + // When the term is not a LocalizedTerm, it means it's newe and should be saved. + if (! $term instanceof LocalizedTerm) { + $term->save(); + } + + return $term; + } + + public function updateOrCreate(array $attributes, array $values = []) + { + $term = $this->firstOrNew($attributes, $values); + + // When the term is a LocalizedTerm, it means it's existing and should be updated. + if ($term instanceof LocalizedTerm) { + if ($slug = Arr::get($values, 'slug')) { + $term->slug($slug); + } + + $term->merge($values); + } + + $term->save(); + + return $term; + } } diff --git a/src/Stache/Repositories/EntryRepository.php b/src/Stache/Repositories/EntryRepository.php index 7078edcea0..f86840ef27 100644 --- a/src/Stache/Repositories/EntryRepository.php +++ b/src/Stache/Repositories/EntryRepository.php @@ -2,6 +2,7 @@ namespace Statamic\Stache\Repositories; +use Closure; use Statamic\Contracts\Entries\Entry; use Statamic\Contracts\Entries\EntryRepository as RepositoryContract; use Statamic\Contracts\Entries\QueryBuilder; @@ -93,6 +94,31 @@ public function findByUri(string $uri, ?string $site = null): ?Entry : $entry; } + public function findOrNew($id) + { + return $this->query()->findOrNew($id); + } + + public function findOr($id, Closure $callback) + { + return $this->query()->findOr($id, $callback); + } + + public function firstOrNew(array $attributes = [], array $values = []) + { + return $this->query()->firstOrNew($attributes, $values); + } + + public function firstOrCreate(array $attributes, array $values = []) + { + return $this->query()->firstOrCreate($attributes, $values); + } + + public function updateOrCreate(array $attributes, array $values = []) + { + return $this->query()->updateOrCreate($attributes, $values); + } + public function save($entry) { if (! $entry->id()) { diff --git a/src/Stache/Repositories/TermRepository.php b/src/Stache/Repositories/TermRepository.php index be09c040ce..824f289fd2 100644 --- a/src/Stache/Repositories/TermRepository.php +++ b/src/Stache/Repositories/TermRepository.php @@ -2,6 +2,7 @@ namespace Statamic\Stache\Repositories; +use Closure; use Statamic\Contracts\Taxonomies\Term; use Statamic\Contracts\Taxonomies\TermRepository as RepositoryContract; use Statamic\Exceptions\TaxonomyNotFoundException; @@ -113,6 +114,31 @@ public function findOrFail($id): Term return $term; } + public function findOrNew($id) + { + return $this->query()->findOrNew($id); + } + + public function findOr($id, Closure $callback) + { + return $this->query()->findOr($id, $callback); + } + + public function firstOrNew(array $attributes, array $values = []) + { + return $this->query()->firstOrNew($attributes, $values); + } + + public function firstOrCreate(array $attributes, array $values = []) + { + return $this->query()->firstOrCreate($attributes, $values); + } + + public function updateOrCreate(array $attributes, array $values = []) + { + return $this->query()->updateOrCreate($attributes, $values); + } + public function save($term) { $this->store diff --git a/tests/Data/Entries/EntryQueryBuilderTest.php b/tests/Data/Entries/EntryQueryBuilderTest.php index cf08300da3..bee0b4ad01 100644 --- a/tests/Data/Entries/EntryQueryBuilderTest.php +++ b/tests/Data/Entries/EntryQueryBuilderTest.php @@ -6,6 +6,7 @@ use Illuminate\Support\Carbon; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; +use Statamic\Contracts\Entries\Entry as EntryContract; use Statamic\Facades\Blueprint; use Statamic\Facades\Collection; use Statamic\Facades\Entry; @@ -897,4 +898,164 @@ public function values_can_be_plucked() 'thing-2', ], Entry::query()->where('type', 'b')->pluck('slug')->all()); } + + /** @test */ + public function entry_can_be_found_using_find_or_new() + { + Collection::make('posts')->save(); + $entry = EntryFactory::collection('posts')->id('hoff')->slug('david-hasselhoff')->data(['title' => 'David Hasselhoff'])->create(); + + $findOrNew = Entry::query() + ->where('collection', 'posts') + ->findOrNew('hoff'); + + $this->assertSame($entry, $findOrNew); + } + + /** @test */ + public function entry_can_be_created_using_find_or_new() + { + Collection::make('posts')->save(); + + $findOrNew = Entry::query() + ->where('collection', 'posts') + ->findOrNew('hoff'); + + $this->assertNull($findOrNew->id()); + $this->assertInstanceOf(EntryContract::class, $findOrNew); + } + + /** @test */ + public function entry_can_be_found_using_find_or() + { + Collection::make('posts')->save(); + $entry = EntryFactory::collection('posts')->id('hoff')->slug('david-hasselhoff')->data(['title' => 'David Hasselhoff'])->create(); + + $findOrNew = Entry::query() + ->where('collection', 'posts') + ->findOr('hoff', function () { + return 'This could be anything.'; + }); + + $this->assertSame($entry, $findOrNew); + } + + /** @test */ + public function callback_is_called_using_find_or() + { + Collection::make('posts')->save(); + + $findOrNew = Entry::query() + ->where('collection', 'posts') + ->findOr('hoff', function () { + return 'This could be anything.'; + }); + + $this->assertSame('This could be anything.', $findOrNew); + } + + /** @test */ + public function entry_can_be_found_using_first_or_new() + { + Collection::make('posts')->save(); + $entry = EntryFactory::collection('posts')->slug('david-hasselhoff')->data(['title' => 'David Hasselhoff'])->create(); + + $firstOrNew = Entry::query() + ->where('collection', 'posts') + ->firstOrNew( + ['slug' => 'david-hasselhoff'], + ['title' => 'David Hasselhoff'] + ); + + $this->assertSame($entry, $firstOrNew); + } + + /** @test */ + public function entry_can_be_created_using_first_or_new() + { + Collection::make('posts')->save(); + + $firstOrNew = Entry::query() + ->where('collection', 'posts') + ->firstOrNew( + ['slug' => 'david-hasselhoff'], + ['title' => 'David Hasselhoff'] + ); + + $this->assertNull($firstOrNew->id()); + $this->assertSame('David Hasselhoff', $firstOrNew->title); + $this->assertSame('david-hasselhoff', $firstOrNew->slug()); + $this->assertSame('posts', $firstOrNew->collection()->handle()); + } + + /** @test */ + public function entry_can_be_found_using_first_or_create() + { + Collection::make('posts')->save(); + $entry = EntryFactory::collection('posts')->slug('david-hasselhoff')->data(['title' => 'David Hasselhoff'])->create(); + + $firstOrCreate = Entry::query() + ->where('collection', 'posts') + ->firstOrCreate( + ['slug' => 'david-hasselhoff'], + ['title' => 'David Hasselhoff'] + ); + + $this->assertSame($entry, $firstOrCreate); + } + + /** @test */ + public function entry_can_be_created_using_first_or_create() + { + Collection::make('posts')->save(); + + $firstOrCreate = Entry::query() + ->where('collection', 'posts') + ->firstOrCreate( + ['slug' => 'david-hasselhoff'], + ['title' => 'David Hasselhoff'] + ); + + $this->assertNotNull($firstOrCreate->id()); + $this->assertSame('David Hasselhoff', $firstOrCreate->title); + $this->assertSame('david-hasselhoff', $firstOrCreate->slug()); + $this->assertSame('posts', $firstOrCreate->collection()->handle()); + } + + /** @test */ + public function entry_can_be_found_and_updated_using_update_or_create() + { + Collection::make('posts')->save(); + $entry = EntryFactory::collection('posts')->slug('david-hasselhoff')->data(['title' => 'David Hasselhoff'])->create(); + + $updateOrCreate = Entry::query() + ->where('collection', 'posts') + ->updateOrCreate( + ['slug' => 'david-hasselhoff'], + ['title' => 'The Hoff'] + ); + + $this->assertSame($entry->id(), $updateOrCreate->id()); + $this->assertSame('The Hoff', $updateOrCreate->title); + $this->assertSame('david-hasselhoff', $updateOrCreate->slug()); + $this->assertSame('posts', $updateOrCreate->collection()->handle()); + } + + /** @test */ + public function entry_can_be_created_using_update_or_create() + { + Collection::make('posts')->save(); + + $updateOrCreate = Entry::query() + ->where('collection', 'posts') + ->updateOrCreate( + ['slug' => 'david-hasselhoff'], + ['title' => 'The Hoff'] + ); + + $this->assertNotNull($updateOrCreate->id()); + $this->assertSame('The Hoff', $updateOrCreate->title); + $this->assertSame('david-hasselhoff', $updateOrCreate->slug()); + $this->assertSame('posts', $updateOrCreate->collection()->handle()); + } } diff --git a/tests/Data/Taxonomies/TermQueryBuilderTest.php b/tests/Data/Taxonomies/TermQueryBuilderTest.php index b09f2406a8..0936c836bb 100644 --- a/tests/Data/Taxonomies/TermQueryBuilderTest.php +++ b/tests/Data/Taxonomies/TermQueryBuilderTest.php @@ -4,6 +4,7 @@ use Facades\Tests\Factories\EntryFactory; use PHPUnit\Framework\Attributes\Test; +use Statamic\Contracts\Taxonomies\Term as TermContract; use Statamic\Facades\Blueprint; use Statamic\Facades\Collection; use Statamic\Facades\Site; @@ -602,4 +603,164 @@ public function terms_are_found_using_offset() $terms = Term::query()->offset(1)->get(); $this->assertEquals(['b', 'c'], $terms->map->slug()->all()); } + + /** @test */ + public function term_can_be_found_using_find_or_new() + { + Taxonomy::make('tags')->save(); + $term = tap(Term::make()->taxonomy('tags')->inDefaultLocale()->slug('alfa')->data(['title' => 'Alfa']))->save(); + + $findOrNew = Term::query() + ->where('taxonomy', 'tags') + ->findOrNew('tags::alfa'); + + $this->assertEquals($term->slug(), $findOrNew->slug()); + } + + /** @test */ + public function term_can_be_created_using_find_or_new() + { + Taxonomy::make('tags')->save(); + + $findOrNew = Term::query() + ->where('taxonomy', 'tags') + ->findOrNew('tags::alfa'); + + $this->assertEmpty($findOrNew->slug()); + $this->assertInstanceOf(TermContract::class, $findOrNew); + } + + /** @test */ + public function term_can_be_found_using_find_or() + { + Taxonomy::make('tags')->save(); + $term = tap(Term::make()->taxonomy('tags')->inDefaultLocale()->slug('alfa')->data(['title' => 'Alfa']))->save(); + + $findOrNew = Term::query() + ->where('taxonomy', 'tags') + ->findOr('tags::alfa', function () { + return 'This could be anything.'; + }); + + $this->assertEquals($term->slug(), $findOrNew->slug()); + } + + /** @test */ + public function callback_is_called_using_find_or() + { + Taxonomy::make('tags')->save(); + + $findOrNew = Term::query() + ->where('taxonomy', 'tags') + ->findOr('tags::alfa', function () { + return 'This could be anything.'; + }); + + $this->assertSame('This could be anything.', $findOrNew); + } + + /** @test */ + public function term_can_be_found_using_first_or_new() + { + Taxonomy::make('tags')->save(); + $term = tap(Term::make()->taxonomy('tags')->inDefaultLocale()->slug('alfa')->data(['title' => 'Alfa']))->save(); + + $firstOrNew = Term::query() + ->where('taxonomy', 'tags') + ->firstOrNew( + ['slug' => 'alfa'], + ['title' => 'Alfa'] + ); + + $this->assertEquals($term->slug(), $firstOrNew->slug()); + } + + /** @test */ + public function term_can_be_created_using_first_or_new() + { + Taxonomy::make('tags')->save(); + + $firstOrNew = Term::query() + ->where('taxonomy', 'tags') + ->firstOrNew( + ['slug' => 'alfa'], + ['title' => 'Alfa'] + ); + + $this->assertNull(Term::find('tags::alfa')); + $this->assertSame('Alfa', $firstOrNew->get('title')); + $this->assertSame('alfa', $firstOrNew->slug()); + $this->assertSame('tags', $firstOrNew->taxonomy()->handle()); + } + + /** @test */ + public function term_can_be_found_using_first_or_create() + { + Taxonomy::make('tags')->save(); + $term = tap(Term::make()->taxonomy('tags')->inDefaultLocale()->slug('alfa')->data(['title' => 'Alfa']))->save(); + + $firstOrCreate = Term::query() + ->where('taxonomy', 'tags') + ->firstOrCreate( + ['slug' => 'alfa'], + ['title' => 'Alfa'] + ); + + $this->assertEquals($term->slug(), $firstOrCreate->slug()); + } + + /** @test */ + public function term_can_be_created_using_first_or_create() + { + Taxonomy::make('tags')->save(); + + $firstOrCreate = Term::query() + ->where('taxonomy', 'tags') + ->firstOrCreate( + ['slug' => 'alfa'], + ['title' => 'Alfa'] + ); + + $this->assertNotNull(Term::find('tags::alfa')); + $this->assertSame('Alfa', $firstOrCreate->get('title')); + $this->assertSame('alfa', $firstOrCreate->slug()); + $this->assertSame('tags', $firstOrCreate->taxonomy()->handle()); + } + + /** @test */ + public function term_can_be_found_and_updated_using_update_or_create() + { + Taxonomy::make('tags')->save(); + $term = tap(Term::make()->taxonomy('tags')->inDefaultLocale()->slug('alfa')->data(['title' => 'Alfa']))->save(); + + $updateOrCreate = Term::query() + ->where('taxonomy', 'tags') + ->updateOrCreate( + ['slug' => 'alfa'], + ['title' => 'Alfa - Updated'] + ); + + $this->assertEquals($term->slug(), $updateOrCreate->slug()); + $this->assertSame('Alfa - Updated', $updateOrCreate->title); + $this->assertSame('alfa', $updateOrCreate->slug()); + $this->assertSame('tags', $updateOrCreate->taxonomy()->handle()); + } + + /** @test */ + public function term_can_be_created_using_update_or_create() + { + Taxonomy::make('tags')->save(); + + $updateOrCreate = Term::query() + ->where('taxonomy', 'tags') + ->updateOrCreate( + ['slug' => 'alfa'], + ['title' => 'Alfa'] + ); + + $this->assertNotNull(Term::find('tags::alfa')); + $this->assertSame('Alfa', $updateOrCreate->get('title')); + $this->assertSame('alfa', $updateOrCreate->slug()); + $this->assertSame('tags', $updateOrCreate->taxonomy()->handle()); + } }