diff --git a/CHANGELOG.md b/CHANGELOG.md index c5aeb79ea..7bee410ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Changed + +- Post factories that are passed a term slug to `with_terms()` will create the + term if it doesn't exist by default. This can be disabled by calling the + `create_terms()` method on the factory or replacing the with terms method call + with `with_terms_only_existing()`. + ### Fixed - Ensure that the `delete()` method of the HTTP Client doesn't set a body by default. +- Ensure that `with_terms()` can support an array of term slugs when passed with a + taxonomy index. ## v1.2.0 - 2024-09-23 diff --git a/src/mantle/database/factory/class-post-factory.php b/src/mantle/database/factory/class-post-factory.php index 99ca79532..6f1b41ad4 100644 --- a/src/mantle/database/factory/class-post-factory.php +++ b/src/mantle/database/factory/class-post-factory.php @@ -36,6 +36,16 @@ class Post_Factory extends Factory { */ protected string $model = Post::class; + /** + * Flag to create terms by default. + */ + protected bool $create_terms = true; + + /** + * Flag to append terms by default. + */ + protected bool $append_terms = true; + /** * Constructor. * @@ -46,21 +56,61 @@ public function __construct( Generator $faker, public string $post_type = 'post' parent::__construct( $faker ); } + /** + * Change the default creation of terms with the post factory. + * + * @param bool $value Value to set. + */ + public function create_terms( bool $value = true ): void { + $this->create_terms = $value; + } + + /** + * Change the default appending of terms with the post factory. + * + * @param bool $value Value to set. + */ + public function append_terms( bool $value = true ): void { + $this->append_terms = $value; + } + /** * Create a new factory instance to create posts with a set of terms. * + * Any slugs passed that are not found will be created. If you want to + * only use existing terms, use `with_terms_only_existing()`. + * * @param array>|\WP_Term|int|string ...$terms Terms to assign to the post. */ public function with_terms( ...$terms ): static { // Handle an array in the first argument. - if ( 1 === count( $terms ) && is_array( $terms[0] ) ) { + if ( 1 === count( $terms ) && isset( $terms[0] ) && is_array( $terms[0] ) ) { + $terms = $terms[0]; + } + + $terms = collect( $terms )->all(); + + return $this->with_middleware( + fn ( array $args, Closure $next ) => $next( $args )->set_terms( $terms, append: $this->append_terms, create: $this->create_terms ), + ); + } + + /** + * Create a new factory instance to create posts with a set of terms without creating + * any unknown terms. + * + * @param array>|\WP_Term|int|string ...$terms Terms to assign to the post. + */ + public function with_terms_only_existing( ...$terms ): static { + // Handle an array in the first argument. + if ( 1 === count( $terms ) && isset( $terms[0] ) && is_array( $terms[0] ) ) { $terms = $terms[0]; } $terms = collect( $terms )->all(); return $this->with_middleware( - fn ( array $args, Closure $next ) => $next( $args )->set_terms( $terms ), + fn ( array $args, Closure $next ) => $next( $args )->set_terms( $terms, append: $this->append_terms, create: false ), ); } diff --git a/src/mantle/database/model/term/trait-model-term.php b/src/mantle/database/model/term/trait-model-term.php index 1025b12e9..d4ca11d05 100644 --- a/src/mantle/database/model/term/trait-model-term.php +++ b/src/mantle/database/model/term/trait-model-term.php @@ -12,6 +12,7 @@ use Mantle\Database\Model\Term; use Mantle\Support\Arr; use Mantle\Support\Collection; +use Mantle\Support\Str; use WP_Term; use function Mantle\Support\Helpers\collect; @@ -120,18 +121,19 @@ public function get_terms( string $taxonomy ): array { * @param mixed $terms Accepts an array of or a single instance of terms. * @param string $taxonomy Taxonomy name, optional. * @param bool $append Append to the object's terms, defaults to false. + * @param bool $create Create the term if it does not exist, defaults to false. * @return static * * @throws Model_Exception Thrown if the $taxonomy cannot be inferred from $terms. * @throws Model_Exception Thrown if error saving the post's terms. */ - public function set_terms( $terms, ?string $taxonomy = null, bool $append = false ) { + public function set_terms( $terms, ?string $taxonomy = null, bool $append = false, bool $create = false ) { $terms = collect( Arr::wrap( $terms ) ); // If taxonomy is not specified, chunk the terms into taxonomy groups. if ( ! $taxonomy ) { $terms = $terms->reduce( - function ( array $carry, $term ): array { + function ( array $carry, $term, $index ) use ( $create ): array { if ( $term instanceof WP_Term ) { $carry[ $term->taxonomy ][] = $term; @@ -165,16 +167,33 @@ function ( array $carry, $term ): array { continue; } + // Use the parent array key as the taxonomy if the parent array + // key is a string and the current array index is not. + if ( ! is_string( $taxonomy ) && is_string( $index ) ) { + $taxonomy = $index; + } + // Attempt to infer if the key is a taxonomy slug and this is a // taxonomy => term slug pair. if ( ! is_string( $taxonomy ) || ! taxonomy_exists( $taxonomy ) ) { continue; } - $item = get_term_object_by( 'slug', $item, $taxonomy ); + $term = get_term_object_by( 'slug', $item, $taxonomy ); + + // Optionally create the term if it does not exist. + if ( ! $term && $create ) { + $term = wp_insert_term( Str::headline( $item ), $taxonomy, [ 'slug' => $item ] ); + + if ( is_wp_error( $term ) ) { + throw new Model_Exception( "Error creating term: [{$term->get_error_message()}]" ); + } + + $term = get_term( $term['term_id'], $taxonomy ); + } - if ( $item ) { - $carry[ $taxonomy ][] = $item; + if ( $term instanceof WP_Term ) { + $carry[ $taxonomy ][] = $term; } } diff --git a/tests/Database/Factory/UnitTestingFactoryTest.php b/tests/Database/Factory/UnitTestingFactoryTest.php index ded85d99e..960d41a9a 100644 --- a/tests/Database/Factory/UnitTestingFactoryTest.php +++ b/tests/Database/Factory/UnitTestingFactoryTest.php @@ -172,6 +172,7 @@ public function test_factory_middleware() { $this->assertEquals( '_test_meta_value', get_post_meta( $post->ID, '_test_meta_key', true ) ); } + #[Group( 'with_terms' )] public function test_posts_with_terms() { $post = static::factory()->post->with_terms( [ @@ -183,6 +184,7 @@ public function test_posts_with_terms() { $this->assertTrue( has_term( $category->term_id, 'category', $post ) ); } + #[Group( 'with_terms' )] public function test_posts_with_term_ids() { $post = static::factory()->post->with_terms( [ @@ -196,15 +198,7 @@ public function test_posts_with_term_ids() { $this->assertTrue( has_term( $category->term_id, 'category', $post ) ); } - public function test_terms_with_posts() { - $post_ids = static::factory()->post->create_many( 2 ); - - $category = static::factory()->category->with_posts( $post_ids )->create_and_get(); - - $this->assertTrue( has_term( $category->term_id, 'category', $post_ids[0] ) ); - $this->assertTrue( has_term( $category->term_id, 'category', $post_ids[1] ) ); - } - + #[Group( 'with_terms' )] public function test_posts_with_terms_multiple_taxonomies() { $post = static::factory()->post->with_terms( $category = static::factory()->category->create_and_get(), @@ -223,7 +217,8 @@ public function test_posts_with_terms_multiple_taxonomies() { $this->assertTrue( has_term( $tag->term_id, 'post_tag', $post ) ); } - public function test_posts_with_terms_multiple_taxonomies_and_term_slug() { + #[Group( 'with_terms' )] + public function test_posts_with_multiple_taxonomies_with_mixed_objects() { $tag = static::factory()->tag->create_and_get(); // Test with the arguments passed as individual parameters. @@ -249,6 +244,84 @@ public function test_posts_with_terms_multiple_taxonomies_and_term_slug() { $this->assertTrue( has_term( $tag->term_id, 'post_tag', $post ) ); } + #[Group( 'with_terms' )] + public function test_posts_with_multiple_terms_single_array_argument() { + $tags = collect( static::factory()->tag->create_many( 5 ) ) + ->map( fn ( $term_id ) => get_term( $term_id ) ) + ->pluck( 'term_id' ) + ->all(); + + $post = static::factory()->post->with_terms( $tags )->create_and_get(); + + $post_tags = get_the_terms( $post, 'post_tag' ); + + $this->assertCount( 5, $post_tags ); + $this->assertEquals( + collect( $tags )->sort()->values()->all(), + collect( $post_tags )->pluck( 'term_id' )->sort()->values()->all(), + ); + } + + #[Group( 'with_terms' )] + public function test_posts_with_multiple_terms_spread_array_argument() { + $tags = collect( static::factory()->tag->create_many( 5 ) ) + ->map( fn ( $term_id ) => get_term( $term_id ) ) + ->pluck( 'term_id' ) + ->all(); + + $post = static::factory()->post->with_terms( ...$tags )->create_and_get(); + + $post_tags = get_the_terms( $post, 'post_tag' ); + + $this->assertCount( 5, $post_tags ); + $this->assertEquals( + collect( $tags )->sort()->values()->all(), + collect( $post_tags )->pluck( 'term_id' )->sort()->values()->all(), + ); + } + + /** + * @dataProvider slug_id_dataprovider + */ + #[DataProvider( 'slug_id_dataprovider' )] + #[Group( 'with_terms' )] + public function test_posts_with_multiple_terms_single_array( string $field ) { + $tags = collect( static::factory()->tag->create_many( 5 ) ) + ->map( fn ( $term_id ) => get_term( $term_id ) ) + ->pluck( $field ) + ->all(); + + $post = static::factory()->post->with_terms( [ 'post_tag' => $tags ] )->create_and_get(); + $post_tags = get_the_terms( $post, 'post_tag' ); + + $this->assertCount( 5, $post_tags ); + $this->assertEquals( + collect( $tags )->sort()->values()->all(), + collect( $post_tags )->pluck( $field )->sort()->values()->all(), + ); + } + + #[Group( 'with_terms' )] + public function test_posts_with_terms_create_unknown_term() { + $post = static::factory()->post->with_terms( [ + 'post_tag' => [ 'unknown-term' ], + ] )->create_and_get(); + + $post_tags = get_the_terms( $post, 'post_tag' ); + + $this->assertCount( 1, $post_tags ); + $this->assertEquals( 'unknown-term', $post_tags[0]->slug ); + } + + public function test_terms_with_posts() { + $post_ids = static::factory()->post->create_many( 2 ); + + $category = static::factory()->category->with_posts( $post_ids )->create_and_get(); + + $this->assertTrue( has_term( $category->term_id, 'category', $post_ids[0] ) ); + $this->assertTrue( has_term( $category->term_id, 'category', $post_ids[1] ) ); + } + public function test_post_with_meta() { $post = static::factory()->post->with_meta( [ @@ -358,6 +431,13 @@ public function test_dynamic_factory_conflict() { static::factory()->conflict->create_and_get(); } + + public static function slug_id_dataprovider(): array { + return [ + 'term_id' => [ 'term_id' ], + 'slug' => [ 'slug' ], + ]; + } } class Testable_Post_Tag extends Term {